package vcs

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"

	"github.com/rs/zerolog/log"
)

// versionPattern restricts version strings to characters that cannot be
// confused with git command-line options. We additionally reject any leading
// '-' to defend against argv option-injection (e.g. "--upload-pack=...").
var versionPattern = regexp.MustCompile(`^v?[0-9A-Za-z.\-+]+$`)

// validateVersion ensures a user-supplied version is safe to pass as an argv
// argument to git. Empty input and option-like prefixes are rejected.
func validateVersion(version string) error {
	if version == "" {
		return fmt.Errorf("version is required")
	}
	if strings.HasPrefix(version, "-") {
		return fmt.Errorf("invalid version %q: must not start with '-'", version)
	}
	if !versionPattern.MatchString(version) {
		return fmt.Errorf("invalid version %q: must match %s", version, versionPattern.String())
	}
	return nil
}

// GitFetcher handles git repository operations
type GitFetcher struct {
	credStore *CredentialStore
	workDir   string
	timeout   time.Duration
}

// NewGitFetcher creates a new git fetcher
func NewGitFetcher(workDir string, credStore *CredentialStore) *GitFetcher {
	if workDir == "" {
		workDir = os.TempDir()
	}

	if credStore == nil {
		credStore = NewCredentialStore()
	}

	return &GitFetcher{
		workDir:   workDir,
		timeout:   30 * time.Second,
		credStore: credStore,
	}
}

// FetchModule clones a git repository and checks out a specific version
// Returns the path to the checked-out source directory
func (g *GitFetcher) FetchModule(ctx context.Context, modulePath, version, credentials string) (string, error) {
	// Validate version before it reaches any git argv to prevent
	// option-injection (e.g. "--upload-pack=..." passed as a "version").
	if err := validateVersion(version); err != nil {
		return "", err
	}

	// Create context with timeout
	ctx, cancel := context.WithTimeout(ctx, g.timeout)
	defer cancel()

	// Parse module path to extract repository URL
	repoURL, err := g.modulePathToRepoURL(modulePath)
	if err != nil {
		return "", err
	}

	// Create temporary directory for this clone
	cloneDir, err := os.MkdirTemp(g.workDir, "gohoarder-git-*")
	if err != nil {
		return "", fmt.Errorf("failed to create temp directory: %w", err)
	}

	log.Debug().
		Str("module", modulePath).
		Str("version", version).
		Str("repo_url", repoURL).
		Str("clone_dir", cloneDir).
		Msg("Fetching module from git")

	// Set up credentials
	credentialHelper, cleanup, err := g.setupCredentials(repoURL, modulePath, credentials)
	if err != nil {
		_ = os.RemoveAll(cloneDir) // #nosec G104 -- Cleanup
		return "", fmt.Errorf("failed to setup credentials: %w", err)
	}
	defer cleanup()

	// Try shallow clone with specific version first (fastest)
	if err := g.shallowClone(ctx, repoURL, version, cloneDir, credentialHelper); err != nil {
		log.Debug().Err(err).Msg("Shallow clone failed, trying full clone")

		// Fallback to full clone
		if err := g.fullClone(ctx, repoURL, cloneDir, credentialHelper); err != nil {
			_ = os.RemoveAll(cloneDir) // #nosec G104 -- Cleanup
			return "", fmt.Errorf("git clone failed: %w", err)
		}

		// Checkout specific version
		if err := g.checkout(ctx, cloneDir, version); err != nil {
			_ = os.RemoveAll(cloneDir) // #nosec G104 -- Cleanup
			return "", fmt.Errorf("git checkout failed: %w", err)
		}
	}

	log.Debug().
		Str("module", modulePath).
		Str("version", version).
		Str("path", cloneDir).
		Msg("Successfully fetched module from git")

	return cloneDir, nil
}

// modulePathToRepoURL converts a Go module path to a git repository URL
// Examples:
//
//	github.com/user/repo → https://github.com/user/repo.git
//	gitlab.com/group/project → https://gitlab.com/group/project.git
func (g *GitFetcher) modulePathToRepoURL(modulePath string) (string, error) {
	// Remove any path components after the repository
	// e.g., github.com/user/repo/v2 → github.com/user/repo
	parts := strings.Split(modulePath, "/")
	if len(parts) < 3 {
		return "", fmt.Errorf("invalid module path: %s", modulePath)
	}

	// For github.com, gitlab.com, bitbucket.org, etc.
	// Format: host/owner/repo
	host := parts[0]
	owner := parts[1]
	repo := parts[2]

	// NOTE: Go module major-version suffixes appear as separate path
	// segments (e.g. "github.com/owner/repo/v2"), never as a "v" prefix on
	// the repo name. Stripping a leading "v" from the repo segment would
	// corrupt legitimate names like "vault", "vitess", "vim-go".

	repoURL := fmt.Sprintf("https://%s/%s/%s.git", host, owner, repo)
	return repoURL, nil
}

// setupCredentials configures git credentials for authentication
// Returns credential helper configuration and cleanup function
func (g *GitFetcher) setupCredentials(repoURL, modulePath, credentials string) (map[string]string, func(), error) {
	env := make(map[string]string)
	cleanup := func() {}

	// Priority 1: Check credential store for pattern-based credentials
	if g.credStore != nil {
		username, token, found := g.credStore.GetCredentialsForModule(modulePath)
		if found {
			log.Debug().
				Str("module", modulePath).
				Msg("Using credentials from credential store")
			return g.createTempNetrc(repoURL, username, token)
		}
	}

	// Priority 2: Use credentials from HTTP Authorization header (if provided)
	if credentials != "" {
		log.Debug().Msg("Using credentials from Authorization header")
		return g.createTempNetrcFromHeader(repoURL, credentials)
	}

	// Priority 3: Rely on system git config (.netrc, etc.)
	log.Debug().Msg("No credentials provided, using system git config")
	return env, cleanup, nil
}

// createTempNetrc creates a temporary .netrc file with the provided credentials
func (g *GitFetcher) createTempNetrc(repoURL, username, token string) (map[string]string, func(), error) {
	// Create temporary .netrc file
	tempDir, err := os.MkdirTemp("", "gohoarder-netrc-*")
	if err != nil {
		return nil, nil, fmt.Errorf("failed to create temp netrc directory: %w", err)
	}

	// Extract host from repo URL
	host := g.extractHost(repoURL)

	// Create .netrc file
	netrcPath := filepath.Join(tempDir, ".netrc")
	netrcContent := fmt.Sprintf("machine %s\nlogin %s\npassword %s\n", host, username, token)
	if err := os.WriteFile(netrcPath, []byte(netrcContent), 0600); err != nil {
		_ = os.RemoveAll(tempDir) // #nosec G104 -- Cleanup
		return nil, nil, fmt.Errorf("failed to write .netrc: %w", err)
	}

	env := map[string]string{
		"HOME":                tempDir,
		"GIT_TERMINAL_PROMPT": "0",
	}

	cleanup := func() {
		_ = os.RemoveAll(tempDir) // #nosec G104 -- Cleanup
	}

	log.Debug().Str("host", host).Msg("Created temporary .netrc for git authentication")

	return env, cleanup, nil
}

// createTempNetrcFromHeader creates a temporary .netrc from Authorization header credentials
func (g *GitFetcher) createTempNetrcFromHeader(repoURL, credentials string) (map[string]string, func(), error) {
	// Extract token from credentials
	token := strings.TrimPrefix(credentials, "Bearer ")
	token = strings.TrimPrefix(token, "Token ")
	token = strings.TrimPrefix(token, "Private-Token ")

	if token == "" || token == credentials {
		// Not in expected format, rely on system config
		log.Debug().Msg("Credentials not in Bearer/Token format, using system git config")
		return make(map[string]string), func() {}, nil
	}

	// Use oauth2 as default username for token-based auth
	return g.createTempNetrc(repoURL, "oauth2", token)
}

// extractHost extracts the git host from a repository URL
func (g *GitFetcher) extractHost(repoURL string) string {
	if strings.Contains(repoURL, "github.com") {
		return "github.com"
	}
	if strings.Contains(repoURL, "gitlab.com") {
		return "gitlab.com"
	}
	if strings.Contains(repoURL, "bitbucket.org") {
		return "bitbucket.org"
	}

	// Generic extraction
	parts := strings.Split(repoURL, "/")
	if len(parts) >= 3 {
		return strings.TrimPrefix(parts[2], "//")
	}

	return ""
}

// shallowClone performs a shallow clone of a specific version
func (g *GitFetcher) shallowClone(ctx context.Context, repoURL, version, cloneDir string, credentialHelper map[string]string) error {
	if err := validateVersion(version); err != nil {
		return err
	}
	// "--" separates options from positional args; combined with version
	// validation it prevents option-injection.
	cmd := exec.CommandContext(ctx, "git", "clone", "--depth", "1", "--branch", version, "--", repoURL, cloneDir)
	cmd.Env = append(os.Environ(), g.envMapToSlice(credentialHelper)...)

	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("shallow clone failed: %w (output: %s)", err, string(output))
	}

	return nil
}

// fullClone performs a full clone of the repository
func (g *GitFetcher) fullClone(ctx context.Context, repoURL, cloneDir string, credentialHelper map[string]string) error {
	cmd := exec.CommandContext(ctx, "git", "clone", "--", repoURL, cloneDir)
	cmd.Env = append(os.Environ(), g.envMapToSlice(credentialHelper)...)

	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("full clone failed: %w (output: %s)", err, string(output))
	}

	return nil
}

// checkout checks out a specific version (tag, branch, or commit) in
// detached-HEAD mode. Detached mode avoids ambiguity between local branch
// refs and remote tags/commits and prevents creating unintended local
// branches when the version happens to share a name with one.
func (g *GitFetcher) checkout(ctx context.Context, repoDir, version string) error {
	if err := validateVersion(version); err != nil {
		return err
	}
	cmd := exec.CommandContext(ctx, "git", "checkout", "--detach", "--", version)
	cmd.Dir = repoDir
	cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")

	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("checkout failed: %w (output: %s)", err, string(output))
	}

	return nil
}

// envMapToSlice converts environment map to slice
func (g *GitFetcher) envMapToSlice(envMap map[string]string) []string {
	var env []string
	for k, v := range envMap {
		env = append(env, fmt.Sprintf("%s=%s", k, v))
	}
	return env
}

// Cleanup removes temporary directories
func (g *GitFetcher) Cleanup(paths ...string) {
	for _, path := range paths {
		if err := os.RemoveAll(path); err != nil {
			log.Warn().Err(err).Str("path", path).Msg("Failed to cleanup temporary directory")
		}
	}
}
