package golang

import (
	"os"
	"path/filepath"
	"strings"

	"github.com/anchore/go-homedir"
	"github.com/anchore/syft/internal/log"
	"github.com/anchore/syft/syft/cataloging"
)

const (
	defaultProxies  = "https://proxy.golang.org,direct"
	directProxyOnly = "direct"
)

var (
	directProxiesOnly = []string{directProxyOnly}
)

type CatalogerConfig struct {
	// SearchLocalModCacheLicenses enables searching for go package licenses in the local GOPATH mod cache.
	// app-config: golang.search-local-mod-cache-licenses
	SearchLocalModCacheLicenses bool `yaml:"search-local-mod-cache-licenses" json:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`

	// LocalModCacheDir specifies the location of the local go module cache directory. When not set, syft will attempt to discover the GOPATH env or default to $HOME/go.
	// app-config: golang.local-mod-cache-dir
	LocalModCacheDir string `yaml:"local-mod-cache-dir" json:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"`

	// SearchLocalVendorLicenses enables searching for go package licenses in the local vendor directory relative to the go.mod file.
	// app-config: golang.search-local-vendor-licenses
	SearchLocalVendorLicenses bool `yaml:"search-local-vendor-licenses" json:"search-local-vendor-licenses" mapstructure:"search-local-vendor-licenses"`

	// LocalVendorDir specifies the location of the local vendor directory. When not set, syft will search for a vendor directory relative to the go.mod file.
	// app-config: golang.local-vendor-dir
	LocalVendorDir string `yaml:"local-vendor-dir" json:"local-vendor-dir" mapstructure:"local-vendor-dir"`

	// SearchRemoteLicenses enables downloading go package licenses from the upstream go proxy (typically proxy.golang.org).
	// app-config: golang.search-remote-licenses
	SearchRemoteLicenses bool `yaml:"search-remote-licenses" json:"search-remote-licenses" mapstructure:"search-remote-licenses"`

	// Proxies is a list of go module proxies to use when fetching go module metadata and licenses. When not set, syft will use the GOPROXY env or default to https://proxy.golang.org,direct.
	// app-config: golang.proxy
	Proxies []string `yaml:"proxies,omitempty" json:"proxies,omitempty" mapstructure:"proxies"`

	// NoProxy is a list of glob patterns that match go module names that should not be fetched from the go proxy. When not set, syft will use the GOPRIVATE and GONOPROXY env vars.
	// app-config: golang.no-proxy
	NoProxy []string `yaml:"no-proxy,omitempty" json:"no-proxy,omitempty" mapstructure:"no-proxy"`

	MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"`

	// CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are
	// "none" (disabled), "stdlib" (only the synthetic stdlib package), "extended-stdlib" (stdlib plus every module
	// under golang.org/x/), and "all" (all module packages plus stdlib).
	// app-config: golang.capture-symbols
	CaptureSymbols cataloging.SymbolScope `yaml:"capture-symbols" json:"capture-symbols" mapstructure:"capture-symbols"`

	// CaptureSymbolsModules is a list of glob patterns (doublestar syntax, where ** crosses path separators
	// and * does not) matched against go module paths. Matching modules get symbols in addition to whatever
	// CaptureSymbols selects, so this can only widen the selection and never narrow it. It has no effect
	// under the "none" scope. A trailing major version suffix is not part of a module's identity, so a
	// pattern matches with or without it: both "github.com/foo/bar" and "github.com/foo/*" select
	// "github.com/foo/bar/v2". A pattern that spells out a suffix selects only that major version.
	// app-config: golang.capture-symbols-modules
	CaptureSymbolsModules []string `yaml:"capture-symbols-modules,omitempty" json:"capture-symbols-modules,omitempty" mapstructure:"capture-symbols-modules"`

	// Whether to use the golang.org/x/tools/go/packages, which executes golang tooling found on the path in addition to potential network access
	UsePackagesLib bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
}

type MainModuleVersionConfig struct {
	// FromLDFlags enables parsing the main module version from the -ldflags build settings.
	// app-config: golang.main-module-version.from-ld-flags
	FromLDFlags bool `yaml:"from-ld-flags" json:"from-ld-flags" mapstructure:"from-ld-flags"`

	// FromContents enables parsing the main module version from the binary contents. This is useful when the version is embedded in the binary but not in the build settings.
	// app-config: golang.main-module-version.from-contents
	FromContents bool `yaml:"from-contents" json:"from-contents" mapstructure:"from-contents"`

	// FromBuildSettings enables parsing the main module version from the go build settings.
	// app-config: golang.main-module-version.from-build-settings
	FromBuildSettings bool `yaml:"from-build-settings" json:"from-build-settings" mapstructure:"from-build-settings"`
}

// DefaultCatalogerConfig create a CatalogerConfig with default options, which includes:
// - setting the default remote proxy if none is provided
// - setting the default no proxy if none is provided
// - setting the default local module cache dir if none is provided
func DefaultCatalogerConfig() CatalogerConfig {
	g := CatalogerConfig{
		UsePackagesLib:    true,
		MainModuleVersion: DefaultMainModuleVersionConfig(),
		LocalModCacheDir:  defaultGoModDir(),
		CaptureSymbols:    cataloging.SymbolScopeNone,
	}

	// first process the proxy settings
	if len(g.Proxies) == 0 {
		goProxy := os.Getenv("GOPROXY")
		if goProxy == "" {
			goProxy = defaultProxies
		}
		g = g.WithProxy(goProxy)
	}

	// next process the gonoproxy settings
	if len(g.NoProxy) == 0 {
		goPrivate := os.Getenv("GOPRIVATE")
		goNoProxy := os.Getenv("GONOPROXY")
		// we only use the env var if it was not set explicitly
		if goPrivate != "" {
			g.NoProxy = append(g.NoProxy, strings.Split(goPrivate, ",")...)
		}

		// next process the goprivate settings; we always add those
		if goNoProxy != "" {
			g.NoProxy = append(g.NoProxy, strings.Split(goNoProxy, ",")...)
		}
	}

	return g
}

// defaultGoModDir returns $GOPATH/pkg/mod or $HOME/go/pkg/mod based on environment variables available
func defaultGoModDir() string {
	goPath := os.Getenv("GOPATH")

	if goPath == "" {
		homeDir, err := homedir.Dir()
		if err != nil {
			log.Debugf("unable to determine GOPATH or user home dir: %w", err)
			return ""
		}
		goPath = filepath.Join(homeDir, "go")
	}

	return filepath.Join(goPath, "pkg", "mod")
}

func DefaultMainModuleVersionConfig() MainModuleVersionConfig {
	return MainModuleVersionConfig{
		FromLDFlags:       true,
		FromContents:      false,
		FromBuildSettings: true,
	}
}

func (g CatalogerConfig) WithSearchLocalModCacheLicenses(input bool) CatalogerConfig {
	g.SearchLocalModCacheLicenses = input
	return g
}

func (g CatalogerConfig) WithLocalModCacheDir(input string) CatalogerConfig {
	if input == "" {
		return g
	}
	g.LocalModCacheDir = input
	return g
}

func (g CatalogerConfig) WithSearchLocalVendorLicenses(input bool) CatalogerConfig {
	g.SearchLocalVendorLicenses = input
	return g
}

func (g CatalogerConfig) WithLocalVendorDir(input string) CatalogerConfig {
	if input == "" {
		return g
	}
	g.LocalVendorDir = input
	return g
}

func (g CatalogerConfig) WithSearchRemoteLicenses(input bool) CatalogerConfig {
	g.SearchRemoteLicenses = input
	return g
}

func (g CatalogerConfig) WithProxy(input string) CatalogerConfig {
	if input == "" {
		return g
	}
	if input == "off" {
		input = directProxyOnly
	}
	g.Proxies = strings.Split(input, ",")
	return g
}

func (g CatalogerConfig) WithNoProxy(input string) CatalogerConfig {
	if input == "" {
		return g
	}
	g.NoProxy = strings.Split(input, ",")
	return g
}

func (g CatalogerConfig) WithMainModuleVersion(input MainModuleVersionConfig) CatalogerConfig {
	g.MainModuleVersion = input
	return g
}

func (g CatalogerConfig) WithCaptureSymbols(input cataloging.SymbolScope) CatalogerConfig {
	g.CaptureSymbols = input
	return g
}

func (g CatalogerConfig) WithCaptureSymbolsModules(input []string) CatalogerConfig {
	g.CaptureSymbolsModules = input
	return g
}

func (g CatalogerConfig) WithUsePackagesLib(useLib bool) CatalogerConfig {
	g.UsePackagesLib = useLib
	return g
}

func (g MainModuleVersionConfig) WithFromLDFlags(input bool) MainModuleVersionConfig {
	g.FromLDFlags = input
	return g
}

func (g MainModuleVersionConfig) WithFromContents(input bool) MainModuleVersionConfig {
	g.FromContents = input
	return g
}

func (g MainModuleVersionConfig) WithFromBuildSettings(input bool) MainModuleVersionConfig {
	g.FromBuildSettings = input
	return g
}
