package kb

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	gogit "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/transport"
	githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
	"github.com/tidbcloud/AgentTeam/internal/service/providertokens"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
)

// AGSHTTPSourceMirrorSyncer mirrors validated external Git remotes into AGS via
// the Git smart HTTP endpoint. It accepts only sanitized remotes; credential_ref
// is resolved at sync runtime and injected through process-local go-git auth
// only for the upstream clone operation.
type AGSHTTPSourceMirrorSyncer struct {
	BaseURL             string
	Handler             http.Handler
	AuthHeader          string
	AuthorizationHeader string
	CredentialResolver  SourceMirrorCredentialResolver
	WorkDir             string
	UpstreamProxyURL    string
	Timeout             time.Duration
	MaxAttempts         int
	RetryDelay          time.Duration
	Logger              *slog.Logger
	PublicRepoCache     PublicRepoCacheResolver
	NativePusher        NativeSourceMirrorPusher
	// ValidateInternalBaseURL is supplied only by the production composition
	// root for process-local embedded AGS Git endpoints.
	ValidateInternalBaseURL func(string) error
}

// NativeSourceMirrorPusher transfers a source mirror through a trusted,
// process-local AGS capability when native Git cannot use the embedded
// meshxags transport.
type NativeSourceMirrorPusher interface {
	PushSourceMirror(context.Context, NativeSourceMirrorPushRequest) error
}

// NativeSourceMirrorPushRequest identifies the MeshX-owned source repository
// and the AGS-owned mirror target without exposing AGS storage paths.
type NativeSourceMirrorPushRequest struct {
	SourceGitDir string
	Target       repos.RepositoryIdentity
	RefSpecs     []string
}

type PublicRepoCacheResolver interface {
	ResolveReady(ctx context.Context, remoteURL string) (PublicRepoCacheResolution, bool, error)
}

type PublicRepoCacheResolution struct {
	ID   string
	Path string
}

// PreflightSourceMirror performs a lightweight ls-remote check against the
// upstream remote to verify that the remote URL is reachable and the
// credential (if any) can read the repository. It does not clone or write
// anything. This is called before AGS repos are provisioned during
// CreateSource to catch token permission errors early and prevent orphan
// AGS repos from partial provisioning failures.
func (s AGSHTTPSourceMirrorSyncer) PreflightSourceMirror(ctx context.Context, request SourceMirrorPreflightRequest) error {
	remoteURL, err := ValidateSourceMirrorRemoteForUpstream(ctx, request.UpstreamType, request.RemoteURL, SourceMirrorRemoteValidationOptions{AllowPrivateHosts: request.AllowPrivateHosts})
	if err != nil {
		return err
	}
	var upstreamAuth transport.AuthMethod
	if accessToken := strings.TrimSpace(request.AccessToken); accessToken != "" {
		upstreamAuth = &githttp.BasicAuth{Username: "x-access-token", Password: accessToken}
	} else if credentialRef := strings.TrimSpace(request.CredentialRef); shouldResolvePreflightCredentialRef(credentialRef, request.RepositoryOwner, request.RepositoryName) {
		if s.CredentialResolver == nil {
			return invalidArgument("source mirror credential_ref resolver is not configured")
		}
		credential, err := s.CredentialResolver.ResolveSourceMirrorCredential(ctx, SourceMirrorCredentialRequest{
			WorkspaceID:     request.WorkspaceID,
			KnowledgeBaseID: request.KnowledgeBaseID,
			SourceID:        request.SourceID,
			CredentialRef:   credentialRef,
			RemoteURL:       remoteURL,
			UpstreamType:    request.UpstreamType,
			RepositoryOwner: request.RepositoryOwner,
			RepositoryName:  request.RepositoryName,
		})
		if err != nil {
			return fmt.Errorf("resolve source mirror credential_ref: %w", err)
		}
		upstreamAuth, err = sourceMirrorGitAuth(credential)
		if err != nil {
			return err
		}
	}
	// Preflight always uses a short timeout independent of the mirror sync
	// timeout. A 30-minute clone timeout is appropriate for large repos but
	// far too long for an ls-remote check that should fail fast.
	cmdCtx, cancel := context.WithTimeout(ctx, defaultSourceMirrorPreflightTimeout)
	defer cancel()
	proxy := sourceMirrorCloneProxyOptions(s.UpstreamProxyURL)
	remote := gogit.NewRemote(nil, &config.RemoteConfig{Name: "origin", URLs: []string{remoteURL}})
	refs, err := remote.ListContext(cmdCtx, &gogit.ListOptions{
		Auth:         upstreamAuth,
		ProxyOptions: proxy,
	})
	if err != nil {
		return sourceMirrorPreflightError(err)
	}
	if len(refs) == 0 {
		return invalidArgument("preflight source mirror remote: no refs returned, the repository may be empty")
	}
	return nil
}

func sourceMirrorPreflightError(err error) error {
	if errors.Is(err, context.DeadlineExceeded) || isRetryableGitMirrorError(err) {
		return codedInvalidArgument(
			"source_mirror_unreachable",
			"Could not reach the repository. Check the URL, network connection, and proxy settings.",
		)
	}
	if errors.Is(err, transport.ErrRepositoryNotFound) {
		return codedInvalidArgument(
			"source_mirror_repository_not_found",
			"Repository was not found. Check the repository URL and access token.",
		)
	}
	if errors.Is(err, transport.ErrAuthorizationFailed) ||
		errors.Is(err, transport.ErrAuthenticationRequired) {
		return codedInvalidArgument(
			"source_mirror_authentication_failed",
			"Could not access the repository. Check the URL and provide an access token with read permission.",
		)
	}
	return codedInvalidArgument(
		"source_mirror_preflight_failed",
		"Could not verify the repository. Check the repository URL and access token, then try again.",
	)
}

func shouldResolvePreflightCredentialRef(credentialRef, repositoryOwner, repositoryName string) bool {
	if _, ok := providertokens.IDFromCredentialRef(credentialRef); ok {
		return true
	}
	return strings.TrimSpace(repositoryOwner) != "" && strings.TrimSpace(repositoryName) != ""
}

func (s AGSHTTPSourceMirrorSyncer) SyncSourceMirror(ctx context.Context, request SourceMirrorSyncRequest) (SourceMirrorSyncResult, error) {
	remoteURL, err := ValidateSourceMirrorRemoteForUpstream(ctx, request.Mirror.UpstreamType, request.RemoteURL, SourceMirrorRemoteValidationOptions{AllowPrivateHosts: request.AllowPrivateHosts})
	if err != nil {
		return SourceMirrorSyncResult{}, err
	}
	credentialOwner, credentialRepo, err := sourceMirrorCredentialOwnerRepo(request)
	if err != nil {
		return SourceMirrorSyncResult{}, err
	}
	var upstreamAuth transport.AuthMethod
	if credentialRef := strings.TrimSpace(request.CredentialRef); credentialRef != "" {
		if s.CredentialResolver == nil {
			return SourceMirrorSyncResult{}, invalidArgument("source mirror credential_ref resolver is not configured")
		}
		credential, err := s.CredentialResolver.ResolveSourceMirrorCredential(ctx, SourceMirrorCredentialRequest{
			WorkspaceID:     request.WorkspaceID,
			KnowledgeBaseID: request.KnowledgeBaseID,
			SourceID:        request.Source.ID,
			CredentialRef:   credentialRef,
			RemoteURL:       remoteURL,
			UpstreamType:    request.Mirror.UpstreamType,
			RepositoryOwner: credentialOwner,
			RepositoryName:  credentialRepo,
		})
		if err != nil {
			return SourceMirrorSyncResult{}, fmt.Errorf("resolve source mirror credential_ref: %w", err)
		}
		upstreamAuth, err = sourceMirrorGitAuth(credential)
		if err != nil {
			return SourceMirrorSyncResult{}, err
		}
	}
	targetURL, err := s.targetGitURL(request.Target)
	if err != nil {
		return SourceMirrorSyncResult{}, err
	}
	workRoot := strings.TrimSpace(s.WorkDir)
	if workRoot == "" {
		workRoot = filepath.Join(os.TempDir(), "meshx-kb-source-mirrors")
	}
	if err := os.MkdirAll(workRoot, 0o700); err != nil {
		return SourceMirrorSyncResult{}, fmt.Errorf("prepare source mirror workspace: %w", err)
	}
	logger := s.Logger
	if logger == nil {
		logger = slog.Default()
	}
	workspace, err := newSourceMirrorWorkspace(workRoot, logger)
	if err != nil {
		return SourceMirrorSyncResult{}, fmt.Errorf("prepare source mirror workspace: %w", err)
	}
	defer workspace.Close()
	timeout := s.Timeout
	if timeout <= 0 {
		timeout = defaultSourceMirrorTimeout
	}
	cmdCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()
	retry := s.gitRetryOptions()
	retry.Progress = request.OnGitProgress
	repoDir := filepath.Join(workspace.path, "repo.git")
	operationLogFields := []any{
		"workspace_id", request.WorkspaceID,
		"knowledge_base_id", request.KnowledgeBaseID,
		"source_id", request.Source.ID,
	}
	repo, err := s.cloneSourceMirror(cmdCtx, retry, request, remoteURL, repoDir, upstreamAuth, logger, operationLogFields)
	if err != nil {
		return SourceMirrorSyncResult{}, err
	}
	resolvedDefaultRef, defaultRefAuto, err := sourceMirrorResolvedDefaultRef(repo, firstNonEmptyString(request.Mirror.DefaultRef, request.Source.DefaultRef), request.Mirror.DefaultRefExplicit)
	if err != nil {
		return SourceMirrorSyncResult{}, err
	}
	targetAuth := gitAuthorizationAuth(s.AuthHeader, s.AuthorizationHeader)
	if targetAuth == nil {
		return SourceMirrorSyncResult{}, unauthorized("Authorization is required for AGS source mirror sync")
	}
	targetControl := agsGitMirrorTargetControl{
		Handler:             s.Handler,
		AuthHeader:          s.AuthHeader,
		AuthorizationHeader: s.AuthorizationHeader,
		Repository:          request.Target,
		NativePusher:        s.NativePusher,
	}
	targetControl.Repository.DefaultBranch = resolvedDefaultRef
	pushStarted := time.Now()
	logger.InfoContext(cmdCtx, "source mirror push started", operationLogFields...)
	if err := pushGoGitMirrorWithRetry(cmdCtx, retry, repo, repoDir, targetURL, targetAuth, targetControl); err != nil {
		logger.ErrorContext(cmdCtx, "source mirror push failed", append(operationLogFields, "elapsed_ms", time.Since(pushStarted).Milliseconds())...)
		return SourceMirrorSyncResult{}, err
	}
	logger.InfoContext(cmdCtx, "source mirror push succeeded", append(operationLogFields, "elapsed_ms", time.Since(pushStarted).Milliseconds())...)
	if err := targetControl.setDefaultBranch(cmdCtx, resolvedDefaultRef); err != nil {
		return SourceMirrorSyncResult{}, err
	}
	return SourceMirrorSyncResult{
		MirroredCommit:     sourceMirrorMirroredCommit(repo, resolvedDefaultRef),
		ResolvedDefaultRef: resolvedDefaultRef,
		DefaultRefAuto:     defaultRefAuto,
	}, nil
}

func (s AGSHTTPSourceMirrorSyncer) cloneSourceMirror(ctx context.Context, retry gitMirrorRetryOptions, request SourceMirrorSyncRequest, remoteURL, repoDir string, auth transport.AuthMethod, logger *slog.Logger, logFields []any) (*gogit.Repository, error) {
	cloneURL := remoteURL
	cloneProxy := sourceMirrorCloneProxyOptions(s.UpstreamProxyURL)
	cloneSource := "upstream"
	// Only unauthenticated public remotes may use the shared cache. A miss or
	// cache lookup failure keeps the existing direct-clone behavior.
	if strings.TrimSpace(request.CredentialRef) == "" && s.PublicRepoCache != nil {
		if cached, found, cacheErr := s.PublicRepoCache.ResolveReady(ctx, remoteURL); cacheErr != nil {
			logger.WarnContext(ctx, "public repository cache lookup failed", "error_class", "cache_lookup_failed")
		} else if found {
			cloneURL = cached.Path
			cloneProxy = transport.ProxyOptions{}
			cloneSource = "cache"
			logger.InfoContext(ctx, "public repository cache hit", append(logFields, "cache_id", cached.ID)...)
		}
	}
	started := time.Now()
	logger.InfoContext(ctx, "source mirror clone started", append(logFields, "clone_source", cloneSource)...)
	repo, err := cloneGoGitMirrorWithRetry(ctx, retry, cloneURL, repoDir, auth, cloneProxy)
	if err != nil {
		logger.ErrorContext(ctx, "source mirror clone failed", append(logFields, "clone_source", cloneSource, "elapsed_ms", time.Since(started).Milliseconds())...)
		return nil, err
	}
	logger.InfoContext(ctx, "source mirror clone succeeded", append(logFields, "clone_source", cloneSource, "elapsed_ms", time.Since(started).Milliseconds())...)
	return repo, nil
}

func (s AGSHTTPSourceMirrorSyncer) targetGitURL(repository repos.RepositoryIdentity) (string, error) {
	rawBase := strings.TrimSpace(s.BaseURL)
	base := strings.TrimRight(rawBase, "/")
	if base == "" {
		return "", invalidArgument("AGS base URL is required for source mirror sync")
	}
	parsed, err := url.Parse(base)
	if err != nil || strings.TrimSpace(parsed.Host) == "" {
		return "", invalidArgument("AGS base URL must be an absolute http(s) URL")
	}
	switch parsed.Scheme {
	case "http", "https":
	case "meshxags":
		if s.ValidateInternalBaseURL == nil || s.ValidateInternalBaseURL(rawBase) != nil {
			return "", invalidArgument("AGS base URL must be an absolute http(s) URL")
		}
	default:
		return "", invalidArgument("AGS base URL must be an absolute http(s) URL")
	}
	owner, repo, err := sourceMirrorTargetOwnerRepo(repository)
	if err != nil {
		return "", err
	}
	return base + "/" + url.PathEscape(owner) + "/" + url.PathEscape(repo) + ".git", nil
}

func sourceMirrorCredentialOwnerRepo(request SourceMirrorSyncRequest) (string, string, error) {
	if strings.TrimSpace(request.CredentialRepository.FullName) != "" || strings.TrimSpace(request.CredentialRepository.Owner) != "" || strings.TrimSpace(request.CredentialRepository.Name) != "" {
		return sourceMirrorTargetOwnerRepo(request.CredentialRepository)
	}
	return sourceMirrorTargetOwnerRepo(request.Target)
}

func sourceMirrorTargetOwnerRepo(repository repos.RepositoryIdentity) (string, string, error) {
	fullName := strings.TrimSpace(repository.FullName)
	if fullName == "" && strings.TrimSpace(repository.Owner) != "" && strings.TrimSpace(repository.Name) != "" {
		fullName = strings.TrimSpace(repository.Owner) + "/" + strings.TrimSpace(repository.Name)
	}
	_, owner, repo, ok := normalizeRepositoryFullName(fullName)
	if !ok {
		return "", "", invalidArgument("target AGS source repository owner/name is required")
	}
	return owner, repo, nil
}

type gitMirrorRetryOptions struct {
	MaxAttempts int
	Delay       time.Duration
	Progress    func(SourceMirrorGitProgress) error
}

func (s AGSHTTPSourceMirrorSyncer) gitRetryOptions() gitMirrorRetryOptions {
	attempts := s.MaxAttempts
	if attempts <= 0 {
		attempts = defaultSourceMirrorGitMaxAttempts
	}
	delay := s.RetryDelay
	if delay <= 0 {
		delay = defaultSourceMirrorGitRetryDelay
	}
	return gitMirrorRetryOptions{MaxAttempts: attempts, Delay: delay}
}

func sourceMirrorCloneProxyOptions(rawURL string) transport.ProxyOptions {
	if trimmed := strings.TrimSpace(rawURL); trimmed != "" {
		return transport.ProxyOptions{URL: trimmed}
	}
	return transport.ProxyOptions{}
}

func cloneGoGitMirrorWithRetry(ctx context.Context, retry gitMirrorRetryOptions, remoteURL string, repoDir string, auth transport.AuthMethod, proxy transport.ProxyOptions) (*gogit.Repository, error) {
	var repo *gogit.Repository
	err := runGoGitMirrorOperationWithRetry(ctx, retry, "clone source mirror remote", true, repoDir, func() error {
		cloned, err := gogit.PlainCloneContext(ctx, repoDir, true, &gogit.CloneOptions{
			URL:          remoteURL,
			Auth:         auth,
			Mirror:       true,
			Progress:     io.Discard,
			ProxyOptions: proxy,
		})
		if err != nil {
			return err
		}
		repo = cloned
		return nil
	})
	if err != nil {
		return nil, err
	}
	return repo, nil
}

func pushGoGitMirrorWithRetry(ctx context.Context, retry gitMirrorRetryOptions, repo *gogit.Repository, repoDir string, targetURL string, auth transport.AuthMethod, control agsGitMirrorTargetControl) error {
	return runGoGitMirrorOperationWithRetry(ctx, retry, "push source mirror to AGS", false, "", func() error {
		remote := gogit.NewRemote(repo.Storer, &config.RemoteConfig{Name: "target", URLs: []string{targetURL}})
		err := remote.PushContext(ctx, &gogit.PushOptions{
			RemoteName: "target",
			Auth:       auth,
			RefSpecs:   sourceMirrorPushRefSpecs(),
			Progress:   io.Discard,
		})
		if err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) && isGoGitBareTreePathError(err) {
			if control.NativePusher != nil {
				refSpecs := sourceMirrorPushRefSpecs()
				nativeRefSpecs := make([]string, 0, len(refSpecs))
				for _, refSpec := range refSpecs {
					nativeRefSpecs = append(nativeRefSpecs, refSpec.String())
				}
				err = control.NativePusher.PushSourceMirror(ctx, NativeSourceMirrorPushRequest{
					SourceGitDir: repoDir,
					Target:       control.Repository,
					RefSpecs:     nativeRefSpecs,
				})
			} else {
				err = pushNativeGitMirror(ctx, repoDir, targetURL, auth)
			}
		}
		if err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) {
			return err
		}
		return pruneGoGitMirrorTargetRefs(ctx, repo, remote, auth, control)
	})
}

// go-git validates tree entry names while calculating objects for a push. That
// worktree-oriented validation rejects some names that native Git can store in
// a bare repository, including a single backslash on Linux and names containing
// control characters. Keep the native fallback limited to those known error
// shapes until go-git can transfer them without applying worktree validation.
func isGoGitBareTreePathError(err error) bool {
	if err == nil {
		return false
	}
	message := err.Error()
	return message == `invalid path: "\\"` ||
		strings.HasPrefix(message, `invalid path "`) &&
			strings.HasSuffix(message, `": contains control character`)
}

func pushNativeGitMirror(ctx context.Context, repoDir, targetURL string, auth transport.AuthMethod) error {
	if strings.TrimSpace(repoDir) == "" {
		return fmt.Errorf("native mirror push requires a repository directory")
	}
	args := []string{"--git-dir", repoDir, "push", "--no-verify", targetURL}
	for _, refspec := range sourceMirrorPushRefSpecs() {
		args = append(args, refspec.String())
	}
	cmd := exec.CommandContext(ctx, "git", args...)
	if auth != nil {
		request, err := http.NewRequest(http.MethodGet, targetURL, nil)
		if err != nil {
			return fmt.Errorf("prepare native mirror push auth: %w", err)
		}
		httpAuth, ok := auth.(interface{ SetAuth(*http.Request) })
		if !ok {
			return fmt.Errorf("native mirror push does not support auth method %s", auth.Name())
		}
		httpAuth.SetAuth(request)
		header, err := sourceMirrorGitHTTPHeader(request.Header)
		if err != nil {
			return err
		}
		cmd.Env = append(os.Environ(),
			"GIT_TERMINAL_PROMPT=0",
			"GIT_CONFIG_COUNT=1",
			"GIT_CONFIG_KEY_0=http.extraHeader",
			"GIT_CONFIG_VALUE_0="+header,
		)
	}
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("native mirror push failed: %w: %s", err, truncateString(redactSourceMirrorGitOutput(string(output), targetURL, auth), 1024))
	}
	return nil
}

func redactSourceMirrorGitOutput(output, targetURL string, auth transport.AuthMethod) string {
	output = strings.ReplaceAll(output, targetURL, "[target]")
	if basic, ok := auth.(*githttp.BasicAuth); ok {
		output = strings.ReplaceAll(output, basic.Username, "[redacted]")
		output = strings.ReplaceAll(output, basic.Password, "[redacted]")
	}
	if token, ok := auth.(*githttp.TokenAuth); ok {
		output = strings.ReplaceAll(output, token.Token, "[redacted]")
	}
	if header, ok := auth.(*bearerHeaderAuth); ok {
		output = strings.ReplaceAll(output, header.value, "[redacted]")
	}
	return strings.TrimSpace(output)
}

func sourceMirrorGitHTTPHeader(headers http.Header) (string, error) {
	var header string
	for name, values := range headers {
		for _, value := range values {
			if header != "" {
				return "", fmt.Errorf("native mirror push requires exactly one auth header")
			}
			header = name + ": " + value
		}
	}
	if header == "" {
		return "", fmt.Errorf("native mirror push auth did not set an HTTP header")
	}
	return header, nil
}

func runGoGitMirrorOperationWithRetry(ctx context.Context, retry gitMirrorRetryOptions, operation string, cleanTarget bool, cleanPath string, run func() error) error {
	if retry.MaxAttempts <= 0 {
		retry.MaxAttempts = 1
	}
	var lastErr error
	for attempt := 1; attempt <= retry.MaxAttempts; attempt++ {
		if retry.Progress != nil {
			if err := retry.Progress(SourceMirrorGitProgress{
				Operation:   sourceMirrorGitOperationKey(operation),
				Attempt:     attempt,
				MaxAttempts: retry.MaxAttempts,
				Retrying:    attempt > 1,
				Failure:     lastErr,
			}); err != nil {
				return fmt.Errorf("record %s attempt %d: %w", operation, attempt, err)
			}
		}
		if cleanTarget && strings.TrimSpace(cleanPath) != "" {
			_ = os.RemoveAll(cleanPath)
		}
		err := run()
		if err == nil {
			return nil
		}
		lastErr = err
		if ctx.Err() != nil || attempt == retry.MaxAttempts || !isRetryableGitMirrorError(err) {
			if attempt == 1 {
				return fmt.Errorf("%s failed: %w", operation, err)
			}
			return fmt.Errorf("%s failed after %d attempts: %w", operation, attempt, err)
		}
		timer := time.NewTimer(retry.Delay)
		select {
		case <-ctx.Done():
			timer.Stop()
			return fmt.Errorf("%s failed after %d attempts: %w", operation, attempt, ctx.Err())
		case <-timer.C:
		}
	}
	return fmt.Errorf("%s failed: %w", operation, lastErr)
}

func sourceMirrorGitOperationKey(operation string) string {
	if strings.Contains(strings.ToLower(operation), "push") {
		return "ags_push"
	}
	return "clone"
}

func isRetryableGitMirrorError(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, transport.ErrAuthorizationFailed) ||
		errors.Is(err, transport.ErrAuthenticationRequired) ||
		errors.Is(err, transport.ErrRepositoryNotFound) ||
		errors.Is(err, transport.ErrEmptyRemoteRepository) ||
		errors.Is(err, config.ErrRefSpecMalformedSeparator) ||
		errors.Is(err, config.ErrRefSpecMalformedWildcard) {
		return false
	}
	message := strings.ToLower(err.Error())
	for _, needle := range nonRetryableGitMirrorMessages() {
		if strings.Contains(message, needle) {
			return false
		}
	}
	for _, needle := range retryableGitMirrorMessages() {
		if strings.Contains(message, needle) {
			return true
		}
	}
	return false
}

func nonRetryableGitMirrorMessages() []string {
	return []string{
		"authentication failed",
		"authentication required",
		"authorization failed",
		"could not read username",
		"repository not found",
		"permission denied",
		"not authorized",
		"invalid username or password",
		"remote rejected",
		"invalid refspec",
		"403",
		"401",
		"404",
	}
}

func retryableGitMirrorMessages() []string {
	return []string{
		"connection reset",
		"connection refused",
		"connection timed out",
		"operation timed out",
		"temporary failure",
		"tls handshake timeout",
		"early eof",
		"unexpected eof",
		"remote end hung up unexpectedly",
		"http/2 stream",
		"status code: 502",
		"status code: 503",
		"status code: 504",
		"returned error: 502",
		"returned error: 503",
		"returned error: 504",
	}
}

func sourceMirrorPushRefSpecs() []config.RefSpec {
	return []config.RefSpec{
		"+refs/heads/*:refs/heads/*",
		"+refs/tags/*:refs/tags/*",
	}
}

func pruneGoGitMirrorTargetRefs(ctx context.Context, repo *gogit.Repository, remote *gogit.Remote, auth transport.AuthMethod, control agsGitMirrorTargetControl) error {
	sourceRefs, err := sourceMirrorLocalRefs(repo)
	if err != nil {
		return err
	}
	targetRefs, err := remote.ListContext(ctx, &gogit.ListOptions{Auth: auth})
	if err != nil {
		return err
	}
	deleteSpecs := make([]config.RefSpec, 0)
	for _, ref := range targetRefs {
		if ref == nil || !sourceMirrorMirroredRefName(ref.Name()) {
			continue
		}
		if _, ok := sourceRefs[ref.Name()]; !ok {
			deleteSpecs = append(deleteSpecs, config.RefSpec(":"+ref.Name().String()))
		}
	}
	if len(deleteSpecs) == 0 {
		return nil
	}
	filteredDeleteSpecs, err := control.filterProtectedDefaultBranchDeletes(ctx, sourceRefs, deleteSpecs)
	if err != nil {
		return err
	}
	if len(filteredDeleteSpecs) == 0 {
		return nil
	}
	if err := remote.PushContext(ctx, &gogit.PushOptions{
		RemoteName: "target",
		Auth:       auth,
		RefSpecs:   filteredDeleteSpecs,
		Progress:   io.Discard,
	}); err != nil && !errors.Is(err, gogit.NoErrAlreadyUpToDate) {
		return err
	}
	return nil
}

type agsGitMirrorTargetControl struct {
	Handler             http.Handler
	AuthHeader          string
	AuthorizationHeader string
	Repository          repos.RepositoryIdentity
	NativePusher        NativeSourceMirrorPusher
}

func (c agsGitMirrorTargetControl) filterProtectedDefaultBranchDeletes(ctx context.Context, sourceRefs map[plumbing.ReferenceName]struct{}, deleteSpecs []config.RefSpec) ([]config.RefSpec, error) {
	targetDefault := strings.TrimSpace(c.Repository.DefaultBranch)
	if targetDefault == "" && c.Handler != nil && strings.TrimSpace(c.AuthorizationHeader) != "" {
		owner, repo, err := sourceMirrorTargetOwnerRepo(c.Repository)
		if err != nil {
			return nil, err
		}
		current, err := c.getDefaultBranch(ctx, owner, repo)
		if err != nil {
			return nil, err
		}
		targetDefault = current
	}
	if targetDefault == "" {
		return deleteSpecs, nil
	}
	if _, ok := sourceRefs[plumbing.NewBranchReferenceName(targetDefault)]; ok {
		return deleteSpecs, nil
	}
	protectedDelete := ":" + plumbing.NewBranchReferenceName(targetDefault).String()
	filtered := deleteSpecs[:0]
	for _, refSpec := range deleteSpecs {
		if refSpec.String() == protectedDelete {
			continue
		}
		filtered = append(filtered, refSpec)
	}
	return filtered, nil
}

func (c agsGitMirrorTargetControl) getDefaultBranch(ctx context.Context, owner string, repo string) (string, error) {
	response := doAGSInternalJSONRequest(ctx, c.Handler, c.AuthHeader, c.AuthorizationHeader, http.MethodGet, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo), nil)
	if response.Code < 200 || response.Code >= 300 {
		return "", fmt.Errorf("read AGS repository default branch: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	var body struct {
		DefaultBranch string `json:"default_branch"`
	}
	if err := json.Unmarshal(response.Body.Bytes(), &body); err != nil {
		return "", fmt.Errorf("decode AGS repository default branch: %w", err)
	}
	return strings.TrimSpace(body.DefaultBranch), nil
}

func (c agsGitMirrorTargetControl) setDefaultBranch(ctx context.Context, defaultBranch string) error {
	owner, repo, err := sourceMirrorTargetOwnerRepo(c.Repository)
	if err != nil {
		return err
	}
	response := doAGSInternalJSONRequest(ctx, c.Handler, c.AuthHeader, c.AuthorizationHeader, http.MethodPatch, "/api/v3/repos/"+url.PathEscape(owner)+"/"+url.PathEscape(repo), map[string]any{
		"default_branch": defaultBranch,
	})
	if response.Code < 200 || response.Code >= 300 {
		return fmt.Errorf("update AGS repository default branch: status %d: %s", response.Code, strings.TrimSpace(response.Body.String()))
	}
	return nil
}

func sourceMirrorLocalRefs(repo *gogit.Repository) (map[plumbing.ReferenceName]struct{}, error) {
	refs := map[plumbing.ReferenceName]struct{}{}
	if repo == nil {
		return refs, nil
	}
	iter, err := repo.References()
	if err != nil {
		return nil, err
	}
	defer iter.Close()
	err = iter.ForEach(func(ref *plumbing.Reference) error {
		if ref != nil && sourceMirrorMirroredRefName(ref.Name()) {
			refs[ref.Name()] = struct{}{}
		}
		return nil
	})
	return refs, err
}

func sourceMirrorMirroredRefName(name plumbing.ReferenceName) bool {
	value := name.String()
	return strings.HasPrefix(value, "refs/heads/") || strings.HasPrefix(value, "refs/tags/")
}

func sourceMirrorMirroredCommit(repo *gogit.Repository, ref string) string {
	if repo == nil {
		return ""
	}
	for _, revision := range sourceMirrorCommitRevisions(ref) {
		hash, err := repo.ResolveRevision(plumbing.Revision(revision))
		if err == nil && hash != nil && !hash.IsZero() {
			return sourceMirrorCommitHash(repo, *hash)
		}
	}
	if strings.TrimSpace(ref) != "" {
		return ""
	}
	head, err := repo.Head()
	if err == nil && !head.Hash().IsZero() {
		return sourceMirrorCommitHash(repo, head.Hash())
	}
	return ""
}

func sourceMirrorResolvedDefaultRef(repo *gogit.Repository, configuredRef string, explicit *bool) (string, bool, error) {
	if repo == nil {
		return "", false, invalidArgument("source mirror remote is empty")
	}
	configuredRef = strings.TrimSpace(configuredRef)
	if configuredRef != "" && (explicit == nil || *explicit) {
		branch := strings.TrimPrefix(configuredRef, "refs/heads/")
		if branch == configuredRef && strings.HasPrefix(configuredRef, "refs/") {
			return "", false, invalidArgument("source mirror default ref must name a branch")
		}
		if _, err := repo.Reference(plumbing.NewBranchReferenceName(branch), true); err == nil {
			return branch, false, nil
		} else if explicit != nil && *explicit {
			return "", false, invalidArgument("source mirror default branch does not exist on the remote")
		}
	}
	if head, err := repo.Head(); err == nil && head.Name().IsBranch() {
		return head.Name().Short(), true, nil
	}
	branches, err := repo.Branches()
	if err != nil {
		return "", false, fmt.Errorf("resolve source mirror default branch: %w", err)
	}
	defer branches.Close()
	var only string
	count := 0
	if err := branches.ForEach(func(ref *plumbing.Reference) error {
		count++
		only = ref.Name().Short()
		return nil
	}); err != nil {
		return "", false, fmt.Errorf("resolve source mirror default branch: %w", err)
	}
	if count == 1 {
		return only, true, nil
	}
	return "", false, invalidArgument("source mirror remote HEAD does not identify a default branch")
}

func sourceMirrorCommitHash(repo *gogit.Repository, hash plumbing.Hash) string {
	if repo == nil || hash.IsZero() {
		return ""
	}
	if commit, err := repo.CommitObject(hash); err == nil {
		return commit.Hash.String()
	}
	if tag, err := repo.TagObject(hash); err == nil && !tag.Target.IsZero() {
		if commit, commitErr := repo.CommitObject(tag.Target); commitErr == nil {
			return commit.Hash.String()
		}
	}
	return hash.String()
}

func sourceMirrorCommitRevisions(ref string) []string {
	ref = strings.TrimSpace(ref)
	revisions := make([]string, 0, 4)
	if ref != "" {
		revisions = append(revisions, ref)
		if !strings.HasPrefix(ref, "refs/") {
			revisions = append(revisions, "refs/heads/"+ref, "refs/tags/"+ref)
		}
	}
	return append(revisions, "HEAD")
}

func sourceMirrorGitAuth(credential SourceMirrorCredential) (transport.AuthMethod, error) {
	username := strings.TrimSpace(credential.Username)
	password := strings.TrimSpace(credential.Password)
	if password != "" {
		if username == "" {
			username = "x-access-token"
		}
		return &githttp.BasicAuth{Username: username, Password: password}, nil
	}
	if header := strings.TrimSpace(credential.AuthorizationHeader); header != "" {
		scheme, token, ok := strings.Cut(header, " ")
		if !ok || !strings.EqualFold(strings.TrimSpace(scheme), "Bearer") || strings.TrimSpace(token) == "" {
			return nil, invalidArgument("source mirror authorization header must be a bearer token")
		}
		return &githttp.TokenAuth{Token: strings.TrimSpace(token)}, nil
	}
	return nil, invalidArgument("source mirror credential_ref resolved without git credentials")
}

func truncateString(value string, limit int) string {
	if limit <= 0 || len(value) <= limit {
		return value
	}
	return value[:limit] + "…"
}
