// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package git

import (
	"context"
	"fmt"
	"strings"

	"github.com/harness/gitness/errors"
	"github.com/harness/gitness/git/api"
	"github.com/harness/gitness/git/check"
	"github.com/harness/gitness/git/hook"
	"github.com/harness/gitness/git/sha"

	"github.com/rs/zerolog/log"
)

type BranchSortOption int

const (
	BranchSortOptionDefault BranchSortOption = iota
	BranchSortOptionName
	BranchSortOptionDate
)

var listBranchesRefFields = []api.GitReferenceField{
	api.GitReferenceFieldRefName,
	api.GitReferenceFieldObjectName,
}

type Branch struct {
	Name   string
	SHA    sha.SHA
	Commit *Commit
}

type CreateBranchParams struct {
	WriteParams
	// BranchName is the name of the branch
	BranchName string
	// Target is a git reference (branch / tag / commit SHA)
	Target string
}

type CreateBranchOutput struct {
	Branch Branch
}

type GetBranchParams struct {
	ReadParams
	// BranchName is the name of the branch
	BranchName string
}

type GetBranchOutput struct {
	Branch Branch
}

type DeleteBranchParams struct {
	WriteParams
	// BranchName is the name of the branch
	BranchName string
}

type ListBranchesParams struct {
	ReadParams
	IncludeCommit bool
	Query         string
	Sort          BranchSortOption
	Order         SortOrder
	Page          int32
	PageSize      int32
}

type ListBranchesOutput struct {
	Branches []Branch
}

func (s *Service) CreateBranch(ctx context.Context, params *CreateBranchParams) (*CreateBranchOutput, error) {
	if err := params.Validate(); err != nil {
		return nil, err
	}
	if err := check.BranchName(params.BranchName); err != nil {
		return nil, errors.InvalidArgument(err.Error())
	}

	repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
	targetCommit, err := s.git.GetCommit(ctx, repoPath, strings.TrimSpace(params.Target))
	if err != nil {
		return nil, fmt.Errorf("failed to get target commit: %w", err)
	}

	branchRef := api.GetReferenceFromBranchName(params.BranchName)

	refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, branchRef)
	if err != nil {
		return nil, fmt.Errorf("failed to create ref updater to create the branch: %w", err)
	}

	err = refUpdater.Do(ctx, sha.Nil, targetCommit.SHA)
	if errors.IsConflict(err) {
		return nil, errors.Conflict("branch %q already exists", params.BranchName)
	}
	if err != nil {
		return nil, fmt.Errorf("failed to create branch reference: %w", err)
	}

	commit, err := mapCommit(targetCommit)
	if err != nil {
		return nil, fmt.Errorf("failed to map git commit: %w", err)
	}

	return &CreateBranchOutput{
		Branch: Branch{
			Name:   params.BranchName,
			SHA:    commit.SHA,
			Commit: commit,
		},
	}, nil
}

func (s *Service) GetBranch(ctx context.Context, params *GetBranchParams) (*GetBranchOutput, error) {
	if params == nil {
		return nil, ErrNoParamsProvided
	}

	repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
	sanitizedBranchName := strings.TrimPrefix(params.BranchName, gitReferenceNamePrefixBranch)

	gitBranch, err := s.git.GetBranch(ctx, repoPath, sanitizedBranchName)
	if err != nil {
		return nil, err
	}

	branch, err := mapBranch(gitBranch)
	if err != nil {
		return nil, fmt.Errorf("failed to map rpc branch %v: %w", gitBranch.Name, err)
	}

	return &GetBranchOutput{
		Branch: *branch,
	}, nil
}

func (s *Service) DeleteBranch(ctx context.Context, params *DeleteBranchParams) error {
	if params == nil {
		return ErrNoParamsProvided
	}

	repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)
	branchRef := api.GetReferenceFromBranchName(params.BranchName)

	refUpdater, err := hook.CreateRefUpdater(s.hookClientFactory, params.EnvVars, repoPath, branchRef)
	if err != nil {
		return fmt.Errorf("failed to create ref updater to create the branch: %w", err)
	}

	err = refUpdater.Do(ctx, sha.None, sha.Nil) // delete whatever is there
	if errors.IsNotFound(err) {
		return errors.NotFound("branch %q does not exist", params.BranchName)
	}
	if err != nil {
		return fmt.Errorf("failed to delete branch reference: %w", err)
	}

	return nil
}

func (s *Service) ListBranches(ctx context.Context, params *ListBranchesParams) (*ListBranchesOutput, error) {
	if params == nil {
		return nil, ErrNoParamsProvided
	}

	repoPath := getFullPathForRepo(s.reposRoot, params.RepoUID)

	gitBranches, err := s.listBranchesLoadReferenceData(ctx, repoPath, api.BranchFilter{
		IncludeCommit: params.IncludeCommit,
		Query:         params.Query,
		Sort:          mapBranchesSortOption(params.Sort),
		Order:         mapToSortOrder(params.Order),
		Page:          params.Page,
		PageSize:      params.PageSize,
	})
	if err != nil {
		return nil, err
	}

	// get commits if needed (single call for perf savings: 1s-4s vs 5s-20s)
	if params.IncludeCommit {
		commitSHAs := make([]string, len(gitBranches))
		for i := range gitBranches {
			commitSHAs[i] = gitBranches[i].SHA.String()
		}

		var gitCommits []*api.Commit
		gitCommits, err = s.git.GetCommits(ctx, repoPath, commitSHAs)
		if err != nil {
			return nil, fmt.Errorf("failed to get commit: %w", err)
		}

		for i := range gitCommits {
			gitBranches[i].Commit = gitCommits[i]
		}
	}

	branches := make([]Branch, len(gitBranches))
	for i, branch := range gitBranches {
		b, err := mapBranch(branch)
		if err != nil {
			return nil, err
		}
		branches[i] = *b
	}

	return &ListBranchesOutput{
		Branches: branches,
	}, nil
}

func (s *Service) listBranchesLoadReferenceData(
	ctx context.Context,
	repoPath string,
	filter api.BranchFilter,
) ([]*api.Branch, error) {
	// TODO: can we be smarter with slice allocation
	branches := make([]*api.Branch, 0, 16)
	handler := listBranchesWalkReferencesHandler(&branches)
	instructor, endsAfter, err := wrapInstructorWithOptionalPagination(
		api.DefaultInstructor, // branches only have one target type, default instructor is enough
		filter.Page,
		filter.PageSize,
	)
	if err != nil {
		return nil, errors.InvalidArgument("invalid pagination details: %v", err)
	}

	opts := &api.WalkReferencesOptions{
		Patterns:   createReferenceWalkPatternsFromQuery(gitReferenceNamePrefixBranch, filter.Query),
		Sort:       filter.Sort,
		Order:      filter.Order,
		Fields:     listBranchesRefFields,
		Instructor: instructor,
		// we don't do any post-filtering, restrict git to only return as many elements as pagination needs.
		MaxWalkDistance: endsAfter,
	}

	err = s.git.WalkReferences(ctx, repoPath, handler, opts)
	if err != nil {
		return nil, fmt.Errorf("failed to walk branch references: %w", err)
	}

	log.Ctx(ctx).Trace().Msgf("git api returned %d branches", len(branches))

	return branches, nil
}

func listBranchesWalkReferencesHandler(
	branches *[]*api.Branch,
) api.WalkReferencesHandler {
	return func(e api.WalkReferencesEntry) error {
		fullRefName, ok := e[api.GitReferenceFieldRefName]
		if !ok {
			return fmt.Errorf("entry missing reference name")
		}
		objectSHA, ok := e[api.GitReferenceFieldObjectName]
		if !ok {
			return fmt.Errorf("entry missing object sha")
		}

		branch := &api.Branch{
			Name: fullRefName[len(gitReferenceNamePrefixBranch):],
			SHA:  sha.Must(objectSHA),
		}

		// TODO: refactor to not use slice pointers?
		*branches = append(*branches, branch)

		return nil
	}
}
