// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.

package service

import (
	"context"
	"strconv"

	"github.com/harness/gitness/gitrpc/internal/types"
	"github.com/harness/gitness/gitrpc/rpc"

	"github.com/rs/zerolog/log"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

func (s RepositoryService) GetCommit(ctx context.Context,
	request *rpc.GetCommitRequest) (*rpc.GetCommitResponse, error) {
	base := request.GetBase()
	if base == nil {
		return nil, types.ErrBaseCannotBeEmpty
	}

	// ensure the provided SHA is valid (and not a reference)
	sha := request.GetSha()
	if !isValidGitSHA(sha) {
		return nil, status.Errorf(codes.InvalidArgument, "the provided commit sha '%s' is of invalid format.", sha)
	}

	repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid())

	gitCommit, err := s.adapter.GetCommit(ctx, repoPath, sha)
	if err != nil {
		return nil, processGitErrorf(err, "failed to get commit")
	}

	commit, err := mapGitCommit(gitCommit)
	if err != nil {
		return nil, status.Errorf(codes.Internal, "failed to map git commit: %v", err)
	}

	return &rpc.GetCommitResponse{
		Commit: commit,
	}, nil
}

func (s RepositoryService) ListCommits(request *rpc.ListCommitsRequest,
	stream rpc.RepositoryService_ListCommitsServer) error {
	base := request.GetBase()
	if base == nil {
		return types.ErrBaseCannotBeEmpty
	}

	ctx := stream.Context()
	repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid())

	gitCommits, renameDetails, err := s.adapter.ListCommits(ctx, repoPath, request.GetGitRef(),
		int(request.GetPage()), int(request.GetLimit()), types.CommitFilter{AfterRef: request.After,
			Path:      request.Path,
			Since:     request.Since,
			Until:     request.Until,
			Committer: request.Committer})
	if err != nil {
		return processGitErrorf(err, "failed to get list of commits")
	}

	// try to get total commits between gitref and After refs
	totalCommits := 0
	if request.Page == 1 && len(gitCommits) < int(request.Limit) {
		totalCommits = len(gitCommits)
	} else if request.After != "" && request.GitRef != request.After {
		div, err := s.adapter.GetCommitDivergences(ctx, repoPath, []types.CommitDivergenceRequest{
			{From: request.GitRef, To: request.After},
		}, 0)
		if err != nil {
			return processGitErrorf(err, "failed to get total commits")
		}
		if len(div) > 0 {
			totalCommits = int(div[0].Ahead)
		}
	}

	log.Ctx(ctx).Trace().Msgf("git adapter returned %d commits", len(gitCommits))
	header := metadata.New(map[string]string{"total-commits": strconv.Itoa(totalCommits)})
	if err := stream.SendHeader(header); err != nil {
		return ErrInternalf("unable to send 'total-commits' header", err)
	}

	for i := range gitCommits {
		var commit *rpc.Commit
		commit, err = mapGitCommit(&gitCommits[i])
		if err != nil {
			return status.Errorf(codes.Internal, "failed to map git commit: %v", err)
		}

		err = stream.Send(&rpc.ListCommitsResponse{
			Commit:        commit,
			RenameDetails: mapRenameDetails(renameDetails),
		})
		if err != nil {
			return status.Errorf(codes.Internal, "failed to send commit: %v", err)
		}
	}

	return nil
}

func (s RepositoryService) GetCommitDivergences(ctx context.Context,
	request *rpc.GetCommitDivergencesRequest) (*rpc.GetCommitDivergencesResponse, error) {
	base := request.GetBase()
	if base == nil {
		return nil, types.ErrBaseCannotBeEmpty
	}

	repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid())

	// map to gitea requests
	requests := request.GetRequests()
	if requests == nil {
		return nil, status.Error(codes.InvalidArgument, "requests is nil")
	}
	giteaDivergenceRequests := make([]types.CommitDivergenceRequest, len(requests))
	for i := range requests {
		if requests[i] == nil {
			return nil, status.Errorf(codes.InvalidArgument, "requests[%d] is nil", i)
		}
		giteaDivergenceRequests[i].From = requests[i].From
		giteaDivergenceRequests[i].To = requests[i].To
	}

	// call gitea
	giteaDivergenceResponses, err := s.adapter.GetCommitDivergences(ctx, repoPath,
		giteaDivergenceRequests, request.GetMaxCount())
	if err != nil {
		return nil, processGitErrorf(err, "failed to get diverging commits")
	}

	// map to rpc response
	response := &rpc.GetCommitDivergencesResponse{
		Divergences: make([]*rpc.CommitDivergence, len(giteaDivergenceResponses)),
	}
	for i := range giteaDivergenceResponses {
		response.Divergences[i] = &rpc.CommitDivergence{
			Ahead:  giteaDivergenceResponses[i].Ahead,
			Behind: giteaDivergenceResponses[i].Behind,
		}
	}

	return response, nil
}

func (s RepositoryService) MergeBase(ctx context.Context,
	r *rpc.MergeBaseRequest,
) (*rpc.MergeBaseResponse, error) {
	base := r.GetBase()
	repoPath := getFullPathForRepo(s.reposRoot, base.GetRepoUid())

	mergeBase, _, err := s.adapter.GetMergeBase(ctx, repoPath, "", r.Ref1, r.Ref2)
	if err != nil {
		return nil, processGitErrorf(err, "failed to find merge base")
	}

	return &rpc.MergeBaseResponse{
		MergeBaseSha: mergeBase,
	}, nil
}