// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package git

import (
	"encoding/base64"
	"fmt"
	"strings"
	"unicode"

	"github.com/go-git/go-git/v5"
	git_config "github.com/go-git/go-git/v5/config"
	git_plumbing "github.com/go-git/go-git/v5/plumbing"
	git_transport "github.com/go-git/go-git/v5/plumbing/transport"
)

// TeaCreateBranch creates a new branch in the repo, tracking from another branch.
func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName string) error {
	// save in .git/config to assign remote for future pulls
	localBranchRefName := git_plumbing.NewBranchReferenceName(localBranchName)
	err := r.CreateBranch(&git_config.Branch{
		Name:   localBranchName,
		Merge:  git_plumbing.NewBranchReferenceName(remoteBranchName),
		Remote: remoteName,
	})
	if err != nil {
		return err
	}

	// serialize the branch to .git/refs/heads
	remoteBranchRefName := git_plumbing.NewRemoteReferenceName(remoteName, remoteBranchName)
	remoteBranchRef, err := r.Storer.Reference(remoteBranchRefName)
	if err != nil {
		return err
	}
	localHashRef := git_plumbing.NewHashReference(localBranchRefName, remoteBranchRef.Hash())
	return r.Storer.SetReference(localHashRef)
}

// TeaCheckout checks out the given branch in the worktree.
func (r TeaRepo) TeaCheckout(ref git_plumbing.ReferenceName) error {
	tree, err := r.Worktree()
	if err != nil {
		return err
	}
	return tree.Checkout(&git.CheckoutOptions{Branch: ref})
}

// TeaDeleteLocalBranch removes the given branch locally
func (r TeaRepo) TeaDeleteLocalBranch(branch *git_config.Branch) error {
	err := r.DeleteBranch(branch.Name)
	// if the branch is not found that's ok, as .git/config may have no entry if
	// no remote tracking branch is configured for it (eg push without -u flag)
	if err != nil && err.Error() != "branch not found" {
		return err
	}
	return r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name))
}

// TeaDeleteRemoteBranch removes the given branch on the given remote via git protocol
func (r TeaRepo) TeaDeleteRemoteBranch(remoteName, remoteBranch string, auth git_transport.AuthMethod) error {
	// delete remote branch via git protocol:
	// an empty source in the refspec means remote deletion to git 🙃
	refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch))
	return r.Push(&git.PushOptions{
		RemoteName: remoteName,
		RefSpecs:   []git_config.RefSpec{git_config.RefSpec(refspec)},
		Prune:      true,
		Auth:       auth,
	})
}

// TeaFindBranchBySha returns a branch that is at the the given SHA and syncs to the
// given remote repo.
func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *git_config.Branch, err error) {
	// find remote matching our repoURL
	remote, err := r.GetRemote(repoURL)
	if err != nil {
		return nil, err
	}
	if remote == nil {
		return nil, fmt.Errorf("no remote found for '%s'", repoURL)
	}
	remoteName := remote.Config().Name

	// check if the given remote has our branch (.git/refs/remotes/<remoteName>/*)
	iter, err := r.References()
	if err != nil {
		return nil, err
	}
	defer iter.Close()
	var remoteRefName git_plumbing.ReferenceName
	var localRefName git_plumbing.ReferenceName
	err = iter.ForEach(func(ref *git_plumbing.Reference) error {
		if ref.Name().IsRemote() {
			name := ref.Name().Short()
			if ref.Hash().String() == sha && strings.HasPrefix(name, remoteName) {
				remoteRefName = ref.Name()
			}
		}

		if ref.Name().IsBranch() && ref.Hash().String() == sha {
			localRefName = ref.Name()
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	if remoteRefName == "" || localRefName == "" {
		// no remote tracking branch found, so a potential local branch
		// can't be a match either
		return nil, nil
	}

	b = &git_config.Branch{
		Remote: remoteName,
		Name:   localRefName.Short(),
		Merge:  localRefName,
	}
	return b, b.Validate()
}

// TeaFindBranchByName returns a branch that is at the the given local and
// remote names and syncs to the given remote repo. This method is less precise
// than TeaFindBranchBySha(), but may be desirable if local and remote branch
// have diverged.
func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config.Branch, err error) {
	// find remote matching our repoURL
	remote, err := r.GetRemote(repoURL)
	if err != nil {
		return nil, err
	}
	if remote == nil {
		return nil, fmt.Errorf("no remote found for '%s'", repoURL)
	}
	remoteName := remote.Config().Name

	// check if the given remote has our branch (.git/refs/remotes/<remoteName>/*)
	iter, err := r.References()
	if err != nil {
		return nil, err
	}
	defer iter.Close()
	var remoteRefName git_plumbing.ReferenceName
	var localRefName git_plumbing.ReferenceName
	remoteSearchingName := fmt.Sprintf("%s/%s", remoteName, branchName)
	err = iter.ForEach(func(ref *git_plumbing.Reference) error {
		if ref.Name().IsRemote() && ref.Name().Short() == remoteSearchingName {
			remoteRefName = ref.Name()
		}
		n := ref.Name()
		if n.IsBranch() && n.Short() == branchName {
			localRefName = n
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	if remoteRefName == "" || localRefName == "" {
		return nil, nil
	}

	b = &git_config.Branch{
		Remote: remoteName,
		Name:   localRefName.Short(),
		Merge:  localRefName,
	}
	return b, b.Validate()
}

// TeaFindBranchRemote gives the first remote that has a branch with the same name or sha,
// depending on what is passed in.
// This function is needed, as git does not always define branches in .git/config with remote entries.
// Priority order is: first match of sha and branch -> first match of branch -> first match of sha
func (r TeaRepo) TeaFindBranchRemote(branchName, hash string) (*git.Remote, error) {
	remotes, err := r.Remotes()
	if err != nil {
		return nil, err
	}

	switch {
	case len(remotes) == 0:
		return nil, nil
	case len(remotes) == 1:
		return remotes[0], nil
	}

	// check if the given remote has our branch (.git/refs/remotes/<remoteName>/*)
	iter, err := r.References()
	if err != nil {
		return nil, err
	}
	defer iter.Close()

	var shaMatch *git.Remote
	var branchMatch *git.Remote
	var fullMatch *git.Remote
	if err := iter.ForEach(func(ref *git_plumbing.Reference) error {
		if ref.Name().IsRemote() {
			names := strings.SplitN(ref.Name().Short(), "/", 2)
			remote := names[0]
			branch := names[1]
			if branchMatch == nil && branchName != "" && branchName == branch {
				if branchMatch, err = r.Remote(remote); err != nil {
					return err
				}
			}
			if shaMatch == nil && hash != "" && hash == ref.Hash().String() {
				if shaMatch, err = r.Remote(remote); err != nil {
					return err
				}
			}
			if fullMatch == nil && branchName != "" && branchName == branch && hash != "" && hash == ref.Hash().String() {
				if fullMatch, err = r.Remote(remote); err != nil {
					return err
				}
				// stop asap you have a full match
				return nil
			}
		}
		return nil
	}); err != nil {
		return nil, err
	}

	if fullMatch != nil {
		return fullMatch, nil
	} else if branchMatch != nil {
		return branchMatch, nil
	} else if shaMatch != nil {
		return shaMatch, nil
	}
	return nil, nil
}

// TeaGetCurrentBranchNameAndSHA return the name and sha of the branch witch is currently active
func (r TeaRepo) TeaGetCurrentBranchNameAndSHA() (string, string, error) {
	localHead, err := r.Head()
	if err != nil {
		return "", "", err
	}

	if !localHead.Name().IsBranch() {
		return "", "", fmt.Errorf("active ref is no branch")
	}

	return localHead.Name().Short(), localHead.Hash().String(), nil
}

// PushToCreatAgitFlowPR pushes the given head to the refs/for/<base>/<topic> ref on the remote to create an agit flow PR.
func (r TeaRepo) PushToCreatAgitFlowPR(remoteName, head, base, topic, title, description string, auth git_transport.AuthMethod) error {
	if !strings.HasPrefix(head, "refs/") {
		head = "refs/heads/" + head
	}

	ref := fmt.Sprintf("%s:refs/for/%s/%s", head, base, topic)

	pushOptions := make(map[string]string)
	if len(title) > 0 {
		pushOptions["title"] = b64Encode(title)
	}
	if len(description) > 0 {
		pushOptions["description"] = b64Encode(description)
	}

	opts := &git.PushOptions{
		RemoteName: remoteName,
		RefSpecs:   []git_config.RefSpec{git_config.RefSpec(ref)},
		Options:    pushOptions,
		Auth:       auth,
	}

	return r.Push(opts)
}

// b64Encode implements base64 encode for string if necessary.
func b64Encode(s string) string {
	if strings.Contains(s, "\n") || !isASCII(s) {
		return "{base64}" + base64.StdEncoding.EncodeToString([]byte(s))
	}
	return s
}

// isASCII indicates string contains only ASCII.
func isASCII(s string) bool {
	for i := 0; i < len(s); i++ {
		if s[i] > unicode.MaxASCII {
			return false
		}
	}
	return true
}
