package git

import (
	"fmt"
	"os/exec"
	"strconv"
	"strings"

	"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log"
)

// CommitOptions holds options for a git commit operation
type CommitOptions struct {
	// CommitMessageText holds a short commit message (-m option)
	CommitMessageText string
	// CommitMessagePath holds the path to a file to be used for the commit message (-F option)
	CommitMessagePath string
	// SigningKey holds a GnuPG key ID or path to a Private SSH Key used to sign the commit with (-S option)
	SigningKey string
	// SigningMethod holds the signing method used to sign commits. (git -c gpg.format=ssh option)
	SigningMethod string
	// SignOff specifies whether to sign-off a commit (-s option)
	SignOff bool
}

// Commit perfoms a git commit for the given pathSpec to the currently checked
// out branch. If pathSpec is empty, or the special value "*", all pending
// changes will be committed. If message is not the empty string, it will be
// used as the commit message, otherwise a default commit message will be used.
// If signingKey is not the empty string, commit will be signed with the given
// GPG or SSH key.
func (m *nativeGitClient) Commit(pathSpec string, opts *CommitOptions) error {
	defaultCommitMsg := "Update parameters"
	// Git configuration
	config := "gpg.format=" + opts.SigningMethod
	args := []string{}
	// -c is a global option and needs to be passed before the actual git sub
	// command (commit).
	if opts.SigningMethod != "" {
		args = append(args, "-c", config)
	}
	args = append(args, "commit")
	if pathSpec == "" || pathSpec == "*" {
		args = append(args, "-a")
	}
	// Commit fails with a space between -S flag and path to SSH key
	// -S/user/test/.ssh/signingKey or -SAAAAAAAA...
	if opts.SigningKey != "" {
		args = append(args, fmt.Sprintf("-S%s", opts.SigningKey))
	}
	if opts.SignOff {
		args = append(args, "-s")
	}
	if opts.CommitMessageText != "" {
		args = append(args, "-m", opts.CommitMessageText)
	} else if opts.CommitMessagePath != "" {
		args = append(args, "-F", opts.CommitMessagePath)
	} else {
		args = append(args, "-m", defaultCommitMsg)
	}

	out, err := m.runCmd(args...)
	if err != nil {
		log.Errorf("%s %v", out, err)
		return err
	}

	return nil
}

// Branch creates a new target branch from a given source branch
func (m *nativeGitClient) Branch(sourceBranch string, targetBranch string) error {
	if sourceBranch != "" {
		_, err := m.runCmd("checkout", sourceBranch)
		if err != nil {
			return fmt.Errorf("could not checkout source branch: %v", err)
		}
	}

	_, err := m.runCmd("branch", targetBranch)
	if err != nil {
		return fmt.Errorf("could not create new branch: %v", err)
	}

	return nil
}

// Push pushes local changes to the remote branch. If force is true, will force
// the remote to accept the push.
func (m *nativeGitClient) Push(remote string, branch string, force bool) error {
	args := []string{"push"}
	if force {
		args = append(args, "-f")
	}
	args = append(args, remote, branch)
	err := m.runCredentialedCmd(args...)
	if err != nil {
		return fmt.Errorf("could not push %s to %s: %v", branch, remote, err)
	}
	return nil
}

// Add adds a path spec to the repository
func (m *nativeGitClient) Add(path string) error {
	return m.runCredentialedCmd("add", path)
}

// SymRefToBranch retrieves the branch name a symbolic ref points to
func (m *nativeGitClient) SymRefToBranch(symRef string) (string, error) {
	output, err := m.runCredentialedCmdWithOutput("remote", "show", "origin")
	if err != nil {
		return "", fmt.Errorf("error running git: %v", err)
	}
	for _, l := range strings.Split(output, "\n") {
		l = strings.TrimSpace(l)
		if strings.HasPrefix(l, "HEAD branch:") {
			b := strings.SplitN(l, ":", 2)
			if len(b) == 2 {
				return strings.TrimSpace(b[1]), nil
			}
		}
	}
	return "", fmt.Errorf("no default branch found in remote")
}

// Config configures username and email address for the repository
func (m *nativeGitClient) Config(username string, email string) error {
	_, err := m.runCmd("config", "user.name", username)
	if err != nil {
		return fmt.Errorf("could not set git username: %v", err)
	}
	_, err = m.runCmd("config", "user.email", email)
	if err != nil {
		return fmt.Errorf("could not set git email: %v", err)
	}

	return nil
}

// runCredentialedCmdWithOutput is a convenience function to run a git command
// with username/password credentials while supplying command output to the
// caller.
// nolint:unparam
func (m *nativeGitClient) runCredentialedCmdWithOutput(args ...string) (string, error) {
	closer, environ, err := m.creds.Environ()
	if err != nil {
		return "", err
	}
	defer func() { _ = closer.Close() }()

	// If a basic auth header is explicitly set, tell Git to send it to the
	// server to force use of basic auth instead of negotiating the auth scheme
	for _, e := range environ {
		if strings.HasPrefix(e, fmt.Sprintf("%s=", forceBasicAuthHeaderEnv)) {
			args = append([]string{"--config-env", fmt.Sprintf("http.extraHeader=%s", forceBasicAuthHeaderEnv)}, args...)
		}
	}

	cmd := exec.Command("git", args...)
	cmd.Env = append(cmd.Env, environ...)
	return m.runCmdOutput(cmd, runOpts{})
}

func (m *nativeGitClient) shallowFetch(revision string, depth int) error {
	var err error
	if revision != "" {
		err = m.runCredentialedCmd("fetch", "origin", revision, "--force", "--prune", "--depth", strconv.Itoa(depth))
	} else {
		err = m.runCredentialedCmd("fetch", "origin", "--force", "--prune", "--depth", strconv.Itoa(depth))
	}
	return err
}

// Fetch fetches latest updates from origin
func (m *nativeGitClient) ShallowFetch(revision string, depth int) error {
	if m.OnFetch != nil {
		done := m.OnFetch(m.repoURL)
		defer done()
	}

	err := m.shallowFetch(revision, depth)

	// When we have LFS support enabled, check for large files and fetch them too.
	// No shallow fetch is possible here
	if err == nil && m.IsLFSEnabled() {
		largeFiles, err := m.LsLargeFiles()
		if err == nil && len(largeFiles) > 0 {
			err = m.runCredentialedCmd("lfs", "fetch", "--all")
			if err != nil {
				return err
			}
		}
	}

	return err
}
