package gitaly

import (
	"context"
	"fmt"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitaly/vendored/gitalypb"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/git"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/grpctool"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/tool/prototool"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/encoding"
	protoenc "google.golang.org/grpc/encoding/proto"
	"google.golang.org/grpc/mem"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
)

const (
	DefaultBranch = git.RefHEAD
)

var (
	_ encoding.CodecV2 = (*pollingCodec)(nil)
)

type PollerInterface interface {
	// Poll performs a poll on the repository.
	// revision can be a branch name or a tag.
	// Poll returns a wrapped context.Canceled, context.DeadlineExceeded or gRPC error if ctx signals done and interrupts a running gRPC call.
	// Poll returns *Error when an error occurs.
	Poll(ctx context.Context, repo *gitalypb.Repository, refName, filepath string) (*PollInfo, error)
}

type Poller struct {
	Client   gitalypb.CommitServiceClient
	Features map[string]string
}

type PollInfo struct {
	CommitID string // empty means ref was not found
}

// Poll searched the given repository for the given fullRefName and returns a PollInfo containing a resolved Commit Object ID.
// Valid fullRefNames are:
// * `refs/heads/*` => for branches
// * `refs/tags/*` => for tags
// * `HEAD` => for the repository's default branch
func (p *Poller) Poll(ctx context.Context, repo *gitalypb.Repository, fullRefName, filepath string) (*PollInfo, error) {
	ctx = appendFeatureFlagsToContext(ctx, p.Features)
	commit, err := p.Client.LastCommitForPath(ctx, &gitalypb.LastCommitForPathRequest{
		Repository: repo,
		Revision:   []byte(fullRefName),
		Path:       []byte(filepath),
		// See https://git-scm.com/docs/git#Documentation/git.txt---literal-pathspecs
		// See https://git-scm.com/docs/git#Documentation/git.txt-codeGITLITERALPATHSPECScode
		LiteralPathspec: true, // no globbing!
	}, grpc.ForceCodecV2(pollingCodec{}))
	if err != nil {
		switch status.Code(err) { //nolint:exhaustive
		case codes.NotFound:
			return nil, NewNotFoundError("LastCommitForPath", fullRefName)
		default:
			return nil, NewRPCError(err, "LastCommitForPath", fullRefName)
		}
	}
	var CommitID string
	if commit.Commit != nil {
		CommitID = commit.Commit.Id
	}
	return &PollInfo{
		CommitID: CommitID,
	}, nil
}

// pollingCodec avoids unmarshaling fields of the LastCommitForPathResponse message that are not needed.
// This reduces CPU usage and generates much less garbage on the heap.
type pollingCodec struct {
}

func (c pollingCodec) Marshal(v any) (mem.BufferSlice, error) {
	return grpctool.ProtoCodec.Marshal(v)
}

func (c pollingCodec) Unmarshal(data mem.BufferSlice, v any) error {
	typedV, ok := v.(*gitalypb.LastCommitForPathResponse)
	if !ok {
		return fmt.Errorf("unexpected message type: %T", v)
	}
	var pollingV LastCommitForPathResponseForPolling
	opts := proto.UnmarshalOptions{
		DiscardUnknown: true, // don't care about other fields, that's the point of this codec!
	}
	err := prototool.UnmarshalProto(opts, data, &pollingV)
	if err != nil {
		return err
	}
	if pollingV.Commit != nil {
		typedV.Commit = &gitalypb.GitCommit{
			Id: pollingV.Commit.Id,
		}
	}
	return nil
}

func (c pollingCodec) Name() string {
	// Pretend to be a codec for protobuf.
	return protoenc.Name
}
