package grpc import ( "context" "log" "sync" "datarush/internal/results/domain" pb "datarush/pkg/api/results" submissionPb "datarush/pkg/api/submission" taskPb "datarush/pkg/api/task" userPb "datarush/pkg/api/user" "github.com/google/uuid" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" ) type IResultService interface { CreateResult(ctx context.Context, result *domain.Result) (*domain.Result, error) GetResult(ctx context.Context, id uuid.UUID) (*domain.Result, error) UpdateResult(ctx context.Context, result *domain.Result) (*domain.Result, error) DeleteResult(ctx context.Context, id uuid.UUID) error ListResults( ctx context.Context, pageSize int32, pageToken int32, competitionID *uuid.UUID, userID *uuid.UUID, status *domain.ResultStatus, minScore *float64, maxScore *float64, ) (results []domain.Result, totalCount int32, nextPageToken int32, err error) GetByCompetitionAndUser(ctx context.Context, competitionID uuid.UUID, userID uuid.UUID) (*domain.Result, error) GetLeaderboard( ctx context.Context, competitionID uuid.UUID, limit int32, offset int32, ) ([]domain.Result, int32, error) UpdateStatus(ctx context.Context, id uuid.UUID, status domain.ResultStatus) (*domain.Result, error) RecalculateRanks(ctx context.Context, competitionID uuid.UUID) error } type ResultsHandler struct { pb.UnimplementedResultsServiceServer service IResultService userServiceAddr string submissionServiceAddr string taskServiceAddr string userClient userPb.UserServiceClient submissionClient submissionPb.SubmissionServiceClient taskClient taskPb.TaskServiceClient mu sync.RWMutex } func NewResultsHandler( s IResultService, userServiceAddr, submissionServiceAddr, taskServiceAddr string, ) *ResultsHandler { return &ResultsHandler{ service: s, userServiceAddr: userServiceAddr, submissionServiceAddr: submissionServiceAddr, taskServiceAddr: taskServiceAddr, } } func (h *ResultsHandler) getUserClient(ctx context.Context) (userPb.UserServiceClient, error) { h.mu.RLock() if h.userClient != nil { client := h.userClient h.mu.RUnlock() return client, nil } h.mu.RUnlock() h.mu.Lock() defer h.mu.Unlock() if h.userClient != nil { return h.userClient, nil } conn, err := grpc.DialContext(ctx, h.userServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) if err != nil { return nil, err } h.userClient = userPb.NewUserServiceClient(conn) return h.userClient, nil } func (h *ResultsHandler) getSubmissionClient(ctx context.Context) (submissionPb.SubmissionServiceClient, error) { h.mu.RLock() if h.submissionClient != nil { client := h.submissionClient h.mu.RUnlock() return client, nil } h.mu.RUnlock() h.mu.Lock() defer h.mu.Unlock() if h.submissionClient != nil { return h.submissionClient, nil } conn, err := grpc.DialContext(ctx, h.submissionServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) if err != nil { return nil, err } h.submissionClient = submissionPb.NewSubmissionServiceClient(conn) return h.submissionClient, nil } func (h *ResultsHandler) getTaskClient(ctx context.Context) (taskPb.TaskServiceClient, error) { h.mu.RLock() if h.taskClient != nil { client := h.taskClient h.mu.RUnlock() return client, nil } h.mu.RUnlock() h.mu.Lock() defer h.mu.Unlock() if h.taskClient != nil { return h.taskClient, nil } conn, err := grpc.DialContext(ctx, h.taskServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) if err != nil { return nil, err } h.taskClient = taskPb.NewTaskServiceClient(conn) return h.taskClient, nil } func (h *ResultsHandler) fetchUsername(ctx context.Context, userID string) string { client, err := h.getUserClient(ctx) if err != nil { log.Printf("failed to get user client: %v", err) return "" } user, err := client.GetProfile(ctx, &userPb.GetProfileRequest{UserId: userID}) if err != nil { log.Printf("failed to get user profile for user %s: %v", userID, err) return "" } return user.GetUsername() } func (h *ResultsHandler) fetchTaskStatuses(ctx context.Context, competitionID, userID string) []*pb.TaskStatus { client, err := h.getSubmissionClient(ctx) if err != nil { log.Printf("failed to get submission client: %v", err) return []*pb.TaskStatus{} } resp, err := client.ListSubmissions(ctx, &submissionPb.ListSubmissionsRequest{ CompetitionId: competitionID, UserId: userID, PageSize: 1000, PageToken: 0, }) if err != nil { log.Printf("failed to list submissions for user %s in competition %s: %v", userID, competitionID, err) return []*pb.TaskStatus{} } taskMap := make(map[string]*pb.TaskStatus) for _, submission := range resp.GetSubmissions() { taskID := submission.GetTaskId() existing, exists := taskMap[taskID] if !exists { taskMap[taskID] = &pb.TaskStatus{ TaskId: taskID, TaskTitle: "", EarnedPoints: submission.GetEarnedPoints(), MaxPoints: 0, Position: nil, } } else { if submission.GetEarnedPoints() > existing.EarnedPoints { existing.EarnedPoints = submission.GetEarnedPoints() } } } taskClient, err := h.getTaskClient(ctx) if err != nil { log.Printf("failed to get task client: %v", err) } else { for taskID, status := range taskMap { task, err := taskClient.GetTask(ctx, &taskPb.GetTaskRequest{TaskId: taskID}) if err != nil { log.Printf("failed to get task %s: %v", taskID, err) continue } status.TaskTitle = task.GetTitle() status.MaxPoints = task.GetMaxPoints() } } taskStatuses := make([]*pb.TaskStatus, 0, len(taskMap)) for _, status := range taskMap { taskStatuses = append(taskStatuses, status) } return taskStatuses } func (h *ResultsHandler) enrichUserResult( ctx context.Context, result *domain.Result, competitionID string, ) *pb.UserResult { userID := result.UserID.String() username := h.fetchUsername(ctx, userID) taskStatuses := h.fetchTaskStatuses(ctx, competitionID, userID) pbResult := &pb.UserResult{ UserId: userID, Username: username, TotalScore: int32(result.Score), OverallPosition: 0, TaskStatuses: taskStatuses, } if result.Rank != nil { pbResult.OverallPosition = int32(*result.Rank) } return pbResult } func (h *ResultsHandler) GetCompetitionResults( ctx context.Context, req *pb.GetCompetitionResultsRequest, ) (*pb.GetCompetitionResultsResponse, error) { competitionID, err := uuid.Parse(req.GetCompetitionId()) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err) } results, total, err := h.service.GetLeaderboard(ctx, competitionID, req.GetPageSize(), req.GetPageToken()) if err != nil { log.Printf("failed to get competition results: %v", err) return nil, status.Errorf(codes.Internal, "failed to get competition results: %v", err) } pbResults := make([]*pb.UserResult, len(results)) var wg sync.WaitGroup for i := range results { wg.Add(1) go func(idx int) { defer wg.Done() pbResults[idx] = h.enrichUserResult(ctx, &results[idx], req.GetCompetitionId()) }(i) } wg.Wait() var nextPageToken int32 if int(req.GetPageToken()+req.GetPageSize()) < int(total) { nextPageToken = req.GetPageToken() + 1 } return &pb.GetCompetitionResultsResponse{ Results: pbResults, TotalCount: total, NextPageToken: nextPageToken, }, nil } func (h *ResultsHandler) GetUserCompetitionResults( ctx context.Context, req *pb.GetUserCompetitionResultsRequest, ) (*pb.GetUserCompetitionResultsResponse, error) { competitionID, err := uuid.Parse(req.GetCompetitionId()) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err) } userID, err := uuid.Parse(req.GetUserId()) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid user id format: %v", err) } result, err := h.service.GetByCompetitionAndUser(ctx, competitionID, userID) if err != nil { if err == domain.ErrResultNotFound { return nil, status.Errorf(codes.NotFound, "result not found for user in competition") } log.Printf("failed to get user competition results: %v", err) return nil, status.Errorf(codes.Internal, "failed to get user competition results: %v", err) } pbResult := h.enrichUserResult(ctx, result, req.GetCompetitionId()) return &pb.GetUserCompetitionResultsResponse{ Result: pbResult, }, nil } func (h *ResultsHandler) RecalculateResults( ctx context.Context, req *pb.RecalculateResultsRequest, ) (*emptypb.Empty, error) { competitionID, err := uuid.Parse(req.GetCompetitionId()) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid competition id format: %v", err) } if err := h.service.RecalculateRanks(ctx, competitionID); err != nil { log.Printf("failed to recalculate results: %v", err) return nil, status.Errorf(codes.Internal, "failed to recalculate results: %v", err) } return &emptypb.Empty{}, nil }