// Package gitmirror provides the shared high-throughput Git bootstrap used by
// both the legacy importer and repository sync. Collaboration metadata is
// planned separately; Git objects and refs stay on the native Git transport.
package gitmirror

import (
	"bufio"
	"context"
	"encoding/base64"
	"fmt"
	"net/url"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"sort"
	"strings"

	"github.com/pingcap/agent-git-service/internal/sync/bulk"
)

// MirrorRepository clones all advertised source refs into a temporary bare
// mirror and pushes them in one native Git transport operation. Credentials are
// injected as process-local HTTP headers and are never embedded in remote URLs.
func MirrorRepository(ctx context.Context, sourceRemote, sourceToken, targetRemote, targetToken, workDir string) error {
	if strings.TrimSpace(sourceToken) == "" || strings.TrimSpace(targetToken) == "" {
		return fmt.Errorf("source and target tokens are required for Git mirroring")
	}
	root := strings.TrimSpace(workDir)
	if root == "" {
		root = os.TempDir()
	}
	dir, err := os.MkdirTemp(root, "ags-sync-mirror-")
	if err != nil {
		return err
	}
	dir, err = filepath.Abs(dir)
	if err != nil {
		return fmt.Errorf("resolve mirror directory: %w", err)
	}
	defer os.RemoveAll(dir)

	mirrorDir := filepath.Join(dir, "repository.git")
	if err := runGitWithToken(ctx, sourceToken, dir, "clone", "--mirror", sourceRemote, mirrorDir); err != nil {
		return fmt.Errorf("clone source mirror: %w", err)
	}
	// GitHub does not advertise pull-request head refs during a normal clone.
	if err := runGitWithToken(ctx, sourceToken, mirrorDir, "fetch", "origin", "+refs/pull/*/head:refs/pull/*/head"); err != nil && !isOptionalPullRefFetchError(err) {
		return fmt.Errorf("fetch source pull request refs: %w", err)
	}
	if err := runGit(ctx, mirrorDir, "remote", "add", "sync-target", targetRemote); err != nil {
		return fmt.Errorf("add target mirror remote: %w", err)
	}
	if err := runGitWithToken(ctx, targetToken, mirrorDir, "push", "--mirror", "sync-target"); err != nil {
		return fmt.Errorf("push target mirror: %w", err)
	}
	return nil
}

// MirrorWritableRefs is the empty-target Git bootstrap shared by repository
// sync's physical executor. It fetches only writable heads/tags, validates that
// every existing target ref is already an identical source ref, and pushes
// missing refs in bounded atomic groups. A retry after a partial process crash
// is therefore idempotent and never overwrites a concurrently created ref.
func MirrorWritableRefs(ctx context.Context, sourceRemote, sourceToken, targetRemote, targetToken, workDir string) error {
	if strings.TrimSpace(sourceRemote) == "" || strings.TrimSpace(targetRemote) == "" {
		return fmt.Errorf("source and target Git remotes are required")
	}
	root := strings.TrimSpace(workDir)
	if root == "" {
		root = os.TempDir()
	}
	dir, err := os.MkdirTemp(root, "ags-sync-writable-refs-")
	if err != nil {
		return err
	}
	defer os.RemoveAll(dir)
	repositoryPath := filepath.Join(dir, "repository.git")
	if err := runGit(ctx, dir, "init", "--bare", repositoryPath); err != nil {
		return err
	}
	if err := runGit(ctx, repositoryPath, "remote", "add", "sync-source", sourceRemote); err != nil {
		return err
	}
	if err := runGitWithToken(ctx, sourceToken, repositoryPath, "fetch", "--prune", "sync-source", "+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*"); err != nil {
		return fmt.Errorf("fetch source writable refs: %w", err)
	}
	sourceRefs, err := localWritableRefs(ctx, repositoryPath)
	if err != nil {
		return err
	}
	targetRefs, err := remoteWritableRefs(ctx, targetRemote, targetToken)
	if err != nil {
		return err
	}
	for ref, oid := range targetRefs {
		if strings.HasPrefix(ref, "refs/heads/ags-sync/pr/") {
			// Reserved shadow refs are managed separately as logical RunItems.
			// They may already exist after a crash between shadow-ref push and
			// verification and must not make native heads/tags bootstrap fail.
			continue
		}
		if sourceRefs[ref] != oid {
			return fmt.Errorf("target ref %s appeared or changed during empty-target bootstrap", ref)
		}
	}
	missing := make([]string, 0, len(sourceRefs)-len(targetRefs))
	for ref := range sourceRefs {
		if _, exists := targetRefs[ref]; !exists {
			missing = append(missing, ref)
		}
	}
	sort.Strings(missing)
	for start := 0; start < len(missing); start += bulk.DefaultMaxItems {
		end := start + bulk.DefaultMaxItems
		if end > len(missing) {
			end = len(missing)
		}
		args := []string{"push", "--atomic", targetRemote}
		for _, ref := range missing[start:end] {
			args = append(args, ref+":"+ref)
		}
		if err := runGitWithToken(ctx, targetToken, repositoryPath, args...); err != nil {
			return fmt.Errorf("push empty-target writable refs: %w", err)
		}
	}
	return nil
}

func localWritableRefs(ctx context.Context, repositoryPath string) (map[string]string, error) {
	command := exec.CommandContext(ctx, "git", "-C", repositoryPath, "for-each-ref", "--format=%(objectname) %(refname)", "refs/heads", "refs/tags")
	output, err := command.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("list local writable refs: %w: %s", err, strings.TrimSpace(string(output)))
	}
	return parseRefs(output), nil
}

func remoteWritableRefs(ctx context.Context, remote, token string) (map[string]string, error) {
	command := exec.CommandContext(ctx, "git", "ls-remote", "--heads", "--tags", remote)
	command.Env = append(os.Environ(), GitHTTPAuthorizationConfig(token)...)
	output, err := command.CombinedOutput()
	if err != nil {
		return nil, fmt.Errorf("list target writable refs: %w: %s", err, strings.TrimSpace(string(output)))
	}
	return parseRefs(output), nil
}

func parseRefs(output []byte) map[string]string {
	result := make(map[string]string)
	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		parts := strings.Fields(scanner.Text())
		if len(parts) != 2 || strings.HasSuffix(parts[1], "^{}") {
			continue
		}
		result[parts[1]] = parts[0]
	}
	return result
}

func runGit(ctx context.Context, dir string, args ...string) error {
	commandArgs := make([]string, 0, len(args)+2)
	if dir != "" {
		commandArgs = append(commandArgs, "-C", dir)
	}
	commandArgs = append(commandArgs, args...)
	cmd := exec.CommandContext(ctx, "git", commandArgs...)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("git %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(string(output)))
	}
	return nil
}

func runGitWithToken(ctx context.Context, token, dir string, args ...string) error {
	commandArgs := make([]string, 0, len(args)+2)
	if dir != "" {
		commandArgs = append(commandArgs, "-C", dir)
	}
	commandArgs = append(commandArgs, args...)
	cmd := exec.CommandContext(ctx, "git", commandArgs...)
	cmd.Env = append(os.Environ(), GitHTTPAuthorizationConfig(token)...)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("git %s: %w: %s", strings.Join(args, " "), err, strings.TrimSpace(string(output)))
	}
	return nil
}

// GitHTTPAuthorizationConfig returns process-local Git config entries for a
// bearer-like token without exposing it in a remote URL or command argument.
func GitHTTPAuthorizationConfig(token string) []string {
	if strings.TrimSpace(token) == "" {
		return nil
	}
	encoded := base64.StdEncoding.EncodeToString([]byte("x-access-token:" + strings.TrimSpace(token)))
	return []string{
		"GIT_CONFIG_COUNT=1",
		"GIT_CONFIG_KEY_0=http.extraHeader",
		"GIT_CONFIG_VALUE_0=Authorization: Basic " + encoded,
	}
}

func isOptionalPullRefFetchError(err error) bool {
	message := strings.ToLower(err.Error())
	return strings.Contains(message, "couldn't find remote ref") || strings.Contains(message, "could not find remote ref")
}

// RemoteForAPIBase converts a GitHub-compatible API base into its Smart HTTP
// Git remote without embedding credentials.
func RemoteForAPIBase(apiURL, owner, repo string) (string, error) {
	base, err := url.Parse(strings.TrimSpace(apiURL))
	if err != nil || base.Scheme == "" || base.Host == "" {
		return "", fmt.Errorf("invalid API URL %q", apiURL)
	}
	if strings.EqualFold(base.Host, "api.github.com") {
		base.Host = "github.com"
		base.Path = ""
	} else {
		base.Path = strings.TrimSuffix(strings.TrimSuffix(strings.TrimRight(base.Path, "/"), "/api/v3"), "/api")
	}
	base.RawQuery = ""
	base.Fragment = ""
	base.Path = path.Join(base.Path, url.PathEscape(owner), url.PathEscape(repo)+".git")
	return base.String(), nil
}
