package packagemanager

import (
	"slices"
	"strings"

	packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
	"golang.org/x/mod/semver"
)

type GoPackageManagerConfig struct {
	CommandName string

	// InstallCommands accept module@version args on the command line, used to
	// extract pinned versions for cooldown reporting.
	InstallCommands []string

	// NonDownloadCommands never load packages and therefore never fetch
	// modules. Deliberately minimal: fmt/clean/vet/fix/doc/mod-graph load
	// packages and can download already-required modules on a cold cache even
	// under -mod=readonly, so they are excluded on purpose and run with the
	// proxy.
	NonDownloadCommands []string
}

func DefaultGoPackageManagerConfig() GoPackageManagerConfig {
	return GoPackageManagerConfig{
		CommandName:         "go",
		InstallCommands:     []string{"get", "install", "run"},
		NonDownloadCommands: []string{"version", "env", "help"},
	}
}

type goPackageManager struct {
	Config GoPackageManagerConfig
}

func NewGoPackageManager(config GoPackageManagerConfig) (*goPackageManager, error) {
	return &goPackageManager{Config: config}, nil
}

var _ PackageManager = &goPackageManager{}

func (g *goPackageManager) Name() string {
	return g.Config.CommandName
}

func (g *goPackageManager) Ecosystem() packagev1.Ecosystem {
	return packagev1.Ecosystem_ECOSYSTEM_GO
}

func (g *goPackageManager) ParseCommand(args []string) (*ParsedCommand, error) {
	if len(args) > 0 && args[0] == g.Config.CommandName {
		args = args[1:]
	}

	parsed := &ParsedCommand{Command: Command{Exe: g.Config.CommandName, Args: args}}

	subcmd, rest := goFirstNonFlagArg(args)
	if subcmd == "" {
		return parsed, nil
	}

	if slices.Contains(g.Config.NonDownloadCommands, subcmd) {
		parsed.IsKnownNonDownloadCommand = true
		return parsed, nil
	}

	if slices.Contains(g.Config.InstallCommands, subcmd) {
		parsed.InstallTargets = goRemoteModuleTargets(rest)
		return parsed, nil
	}

	if subcmd == "mod" {
		modCmd, modRest := goFirstNonFlagArg(rest)
		switch modCmd {
		case "tidy":
			parsed.IsManifestInstall = true
			parsed.ManifestFiles = []string{"go.mod"}
		case "download":
			parsed.InstallTargets = goRemoteModuleTargets(modRest)
			if len(parsed.InstallTargets) == 0 {
				parsed.IsManifestInstall = true
				parsed.ManifestFiles = []string{"go.mod"}
			}
		}
	}

	return parsed, nil
}

func goFirstNonFlagArg(args []string) (string, []string) {
	for i, arg := range args {
		if strings.HasPrefix(arg, "-") {
			continue
		}
		return arg, args[i+1:]
	}
	return "", nil
}

// goRemoteModuleTargets extracts remote module targets (module[@version]) from
// command args, skipping flags, local paths and meta-patterns. Version queries
// (@latest, branch names, commit hashes) are passed through for go to resolve;
// only canonical semver counts as an explicit version for cooldown reporting.
func goRemoteModuleTargets(args []string) []*PackageInstallTarget {
	var targets []*PackageInstallTarget

	for _, arg := range args {
		if strings.HasPrefix(arg, "-") || !isGoRemoteModuleTarget(arg) {
			continue
		}

		name, version := arg, ""
		if at := strings.LastIndex(arg, "@"); at > 0 {
			name, version = arg[:at], arg[at+1:]
		}

		targets = append(targets, &PackageInstallTarget{
			PackageVersion: &packagev1.PackageVersion{
				Package: &packagev1.Package{
					Ecosystem: packagev1.Ecosystem_ECOSYSTEM_GO,
					Name:      name,
				},
				Version: version,
			},
			IsExplicitVersion: semver.IsValid(version) && semver.Canonical(version) == version,
		})
	}

	return targets
}

// isGoRemoteModuleTarget discriminates a remote module target from a local
// path or meta-pattern: a remote target's first path segment is a domain
// (contains a dot), so `go install ./cmd/foo` and `go build ./...` yield no
// targets while `go get github.com/x/y@v1.2.3` does.
func isGoRemoteModuleTarget(target string) bool {
	target = strings.TrimSpace(target)
	if target == "" || target == "." || target == ".." {
		return false
	}

	if strings.HasPrefix(target, "./") || strings.HasPrefix(target, "../") || strings.HasPrefix(target, "/") {
		return false
	}

	if strings.Contains(target, `\`) {
		return false
	}

	firstSegment, _, _ := strings.Cut(target, "/")
	if firstSegment == "..." {
		return false
	}

	return strings.Contains(firstSegment, ".")
}
