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

package git

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"sort"
	"strconv"
	"strings"
)

type cliBackend struct{}

type cliRepository struct {
	workTree string
}

func (cliBackend) Name() string {
	return "cli"
}

func (b cliBackend) Open(path string) (RepositoryBackend, error) {
	if path == "" {
		path = "."
	}

	out, err := runGitCommand(path, nil, nil, "rev-parse", "--show-toplevel")
	if err != nil {
		return nil, classifyRepoError(err)
	}

	return &cliRepository{workTree: strings.TrimSpace(out)}, nil
}

func (b cliBackend) Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (RepositoryBackend, error) {
	extraConfigs := make([]string, 0, 1)
	if opts.Insecure {
		extraConfigs = append(extraConfigs, "http.sslVerify=false")
	}

	args := []string{"clone"}
	if opts.Depth > 0 {
		args = append(args, "--depth", strconv.Itoa(opts.Depth))
	}
	args = append(args, remoteURL, path)

	if _, err := runGitCommand("", auth, extraConfigs, args...); err != nil {
		return nil, err
	}
	return b.Open(path)
}

func (r *cliRepository) WorkTree() string {
	return r.workTree
}

func (r *cliRepository) Config() (*Config, error) {
	cfg := &Config{
		Remotes:  map[string]*RemoteConfig{},
		Branches: map[string]*Branch{},
	}

	remoteOut, err := r.git(nil, nil, "remote")
	if err != nil {
		return nil, err
	}
	for _, remoteName := range strings.Fields(remoteOut) {
		urlOut, err := r.git(nil, nil, "config", "--get-all", "remote."+remoteName+".url")
		if err != nil {
			return nil, err
		}
		cfg.Remotes[remoteName] = &RemoteConfig{Name: remoteName, URLs: splitNonEmptyLines(urlOut)}
	}

	branchOut, err := r.git(nil, nil, "config", "--get-regexp", `^branch\..*\.(remote|merge)$`)
	if err != nil {
		var gitErr *gitCommandError
		if !errors.As(err, &gitErr) {
			return nil, err
		}
		if strings.TrimSpace(gitErr.stderr) != "" && !strings.Contains(gitErr.stderr, "No such section or key") {
			return nil, err
		}
		return cfg, nil
	}

	for _, line := range splitNonEmptyLines(branchOut) {
		parts := strings.SplitN(line, " ", 2)
		if len(parts) != 2 {
			continue
		}
		branchName, field, ok := parseBranchConfigKey(parts[0])
		if !ok {
			continue
		}
		branch := cfg.Branches[branchName]
		if branch == nil {
			branch = &Branch{Name: branchName}
			cfg.Branches[branchName] = branch
		}
		switch field {
		case "remote":
			branch.Remote = parts[1]
		case "merge":
			branch.Merge = ReferenceName(parts[1])
		}
	}

	return cfg, nil
}

func (r *cliRepository) Head() (*Reference, error) {
	hashOut, err := r.git(nil, nil, "rev-parse", "HEAD")
	if err != nil {
		return nil, err
	}
	hash := Hash(strings.TrimSpace(hashOut))

	if refOut, err := r.git(nil, nil, "symbolic-ref", "-q", "HEAD"); err == nil {
		return &Reference{name: ReferenceName(strings.TrimSpace(refOut)), hash: hash}, nil
	}
	if tagOut, err := r.git(nil, nil, "describe", "--exact-match", "--tags", "HEAD"); err == nil {
		return &Reference{name: ReferenceName("refs/tags/" + strings.TrimSpace(tagOut)), hash: hash}, nil
	}
	return &Reference{name: ReferenceName("HEAD"), hash: hash}, nil
}

func (r *cliRepository) AddRemote(name, remoteURL string) error {
	_, err := r.git(nil, nil, "remote", "add", name, remoteURL)
	return err
}

func (r *cliRepository) SetBranchUpstream(branchName, remoteName, remoteBranch string) error {
	mergeRef := NewBranchReferenceName(remoteBranch).String()
	if _, err := r.git(nil, nil, "config", "branch."+branchName+".remote", remoteName); err != nil {
		return err
	}
	_, err := r.git(nil, nil, "config", "branch."+branchName+".merge", mergeRef)
	return err
}

func (r *cliRepository) Fetch(remoteName string, refspecs []string, auth *AuthMethod) error {
	args := []string{"fetch", remoteName}
	args = append(args, refspecs...)
	_, err := r.git(auth, nil, args...)
	return err
}

func (r *cliRepository) CreateTrackingBranch(localBranchName, remoteBranchName, remoteName string) error {
	_, err := r.git(nil, nil, "branch", "--track", localBranchName, fmt.Sprintf("%s/%s", remoteName, remoteBranchName))
	if err == nil {
		return nil
	}
	if gitErr, ok := err.(*gitCommandError); ok && strings.Contains(gitErr.stderr, "already exists") {
		return ErrBranchExists
	}
	return err
}

func (r *cliRepository) Checkout(ref ReferenceName) error {
	_, err := r.git(nil, nil, "checkout", ref.String())
	return err
}

func (r *cliRepository) DeleteLocalBranch(branchName string) error {
	_, err := r.git(nil, nil, "branch", "-D", branchName)
	if err == nil {
		return nil
	}
	if gitErr, ok := err.(*gitCommandError); ok {
		stderr := strings.ToLower(gitErr.stderr)
		if strings.Contains(stderr, "not found") || strings.Contains(stderr, "not exist") {
			return nil
		}
	}
	return err
}

func (r *cliRepository) DeleteRemoteBranch(remoteName, remoteBranch string, auth *AuthMethod) error {
	_, err := r.git(auth, nil, "push", "--delete", remoteName, remoteBranch)
	return err
}

func (r *cliRepository) ListReferences(prefixes ...string) ([]*Reference, error) {
	args := []string{"for-each-ref", "--format=%(objectname)%09%(refname)"}
	args = append(args, prefixes...)
	out, err := r.git(nil, nil, args...)
	if err != nil {
		return nil, err
	}

	refs := make([]*Reference, 0)
	for _, line := range splitNonEmptyLines(out) {
		parts := strings.SplitN(line, "	", 2)
		if len(parts) != 2 {
			continue
		}
		refs = append(refs, &Reference{name: ReferenceName(parts[1]), hash: Hash(parts[0])})
	}
	return refs, nil
}

func (r *cliRepository) PushToAgitFlowPR(remoteName, head, base, topic string, pushOptions map[string]string, auth *AuthMethod) error {
	ref := fmt.Sprintf("%s:refs/for/%s/%s", head, base, topic)
	args := []string{"push", remoteName, ref}
	if len(pushOptions) > 0 {
		keys := make([]string, 0, len(pushOptions))
		for key := range pushOptions {
			keys = append(keys, key)
		}
		sort.Strings(keys)
		for _, key := range keys {
			args = append(args, "-o", key+"="+pushOptions[key])
		}
	}
	_, err := r.git(auth, nil, args...)
	return err
}

func (r *cliRepository) git(auth *AuthMethod, extraConfigs []string, args ...string) (string, error) {
	return runGitCommand(r.workTree, auth, extraConfigs, args...)
}

type gitCommandError struct {
	args   []string
	stderr string
	err    error
}

func (e *gitCommandError) Error() string {
	stderr := strings.TrimSpace(e.stderr)
	if stderr == "" {
		return fmt.Sprintf("git %s: %v", strings.Join(e.args, " "), e.err)
	}
	return fmt.Sprintf("git %s: %s", strings.Join(e.args, " "), stderr)
}

func (e *gitCommandError) Unwrap() error {
	return e.err
}

func runGitCommand(dir string, auth *AuthMethod, extraConfigs []string, args ...string) (string, error) {
	authConfigs, authEnv, cleanup, err := prepareCLIAuth(auth)
	if err != nil {
		return "", err
	}
	defer cleanup()

	fullArgs := make([]string, 0, (len(extraConfigs)+len(authConfigs))*2+len(args))
	for _, cfg := range extraConfigs {
		fullArgs = append(fullArgs, "-c", cfg)
	}
	for _, cfg := range authConfigs {
		fullArgs = append(fullArgs, "-c", cfg)
	}
	fullArgs = append(fullArgs, args...)

	cmd := exec.Command("git", fullArgs...)
	if dir != "" {
		cmd.Dir = dir
	}
	cmd.Env = append(os.Environ(), authEnv...)

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		return "", &gitCommandError{args: fullArgs, stderr: stderr.String(), err: err}
	}
	return stdout.String(), nil
}

func prepareCLIAuth(auth *AuthMethod) ([]string, []string, func(), error) {
	if auth == nil {
		return nil, nil, func() {}, nil
	}

	configs := make([]string, 0, 1)
	env := make([]string, 0, 4)
	cleanup := func() {}

	switch auth.Scheme {
	case "http", "https":
		if auth.Username != "" || auth.Password != "" {
			header := "Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth.Username+":"+auth.Password))
			configs = append(configs, "http.extraHeader="+header)
		}
	case "ssh":
		sshCommand := "ssh"
		if auth.KeyFile != "" {
			sshCommand += " -i " + shellQuote(auth.KeyFile) + " -o IdentitiesOnly=yes"
		}
		env = append(env, "GIT_SSH_COMMAND="+sshCommand)
		if auth.KeyPassphrase != "" {
			askPassPath, err := writeAskPassScript(auth.KeyPassphrase)
			if err != nil {
				return nil, nil, cleanup, err
			}
			cleanup = func() { _ = os.Remove(askPassPath) }
			env = append(env,
				"SSH_ASKPASS="+askPassPath,
				"SSH_ASKPASS_REQUIRE=force",
				"DISPLAY=tea",
			)
		}
	}

	return configs, env, cleanup, nil
}

func writeAskPassScript(passphrase string) (string, error) {
	f, err := os.CreateTemp("", "tea-ssh-askpass-*")
	if err != nil {
		return "", err
	}
	defer f.Close()

	content := "#!/bin/sh\nprintf '%s\\n' " + shellQuote(passphrase) + "\n"
	if _, err := f.WriteString(content); err != nil {
		_ = os.Remove(f.Name())
		return "", err
	}
	if err := f.Chmod(0o700); err != nil {
		_ = os.Remove(f.Name())
		return "", err
	}
	return f.Name(), nil
}

func classifyRepoError(err error) error {
	var gitErr *gitCommandError
	if errors.As(err, &gitErr) {
		msg := strings.ToLower(gitErr.stderr)
		if strings.Contains(msg, "not a git repository") || strings.Contains(msg, "cannot change to") {
			return ErrRepositoryNotExists
		}
	}
	return err
}

func parseBranchConfigKey(key string) (branchName, field string, ok bool) {
	const prefix = "branch."
	if !strings.HasPrefix(key, prefix) {
		return "", "", false
	}
	trimmed := strings.TrimPrefix(key, prefix)
	switch {
	case strings.HasSuffix(trimmed, ".remote"):
		return strings.TrimSuffix(trimmed, ".remote"), "remote", true
	case strings.HasSuffix(trimmed, ".merge"):
		return strings.TrimSuffix(trimmed, ".merge"), "merge", true
	default:
		return "", "", false
	}
}

func splitNonEmptyLines(s string) []string {
	lines := strings.Split(strings.TrimSpace(s), "\n")
	out := make([]string, 0, len(lines))
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line != "" {
			out = append(out, line)
		}
	}
	return out
}
