package runner

import (
	"errors"
	"flag"
	"fmt"
	"os"
	"os/exec"
	pathpkg "path"
	"path/filepath"
	"reflect"
	"regexp"
	"runtime"
	"strings"
	"time"

	"dario.cat/mergo"
	"github.com/pelletier/go-toml"
)

const (
	dftTOML = ".air.toml"
	airWd   = "air_wd"

	defaultProxyAppStartTimeout = 5000

	schemaHeader = "#:schema https://json.schemastore.org/any.json"
)

// Config is the main configuration structure for Air.
type Config struct {
	Root        string    `toml:"root" usage:"Working directory, . or absolute path, please note that the directories following must be under root"`
	TmpDir      string    `toml:"tmp_dir" usage:"Temporary directory for air"`
	TestDataDir string    `toml:"testdata_dir"`
	EnvFiles    []string  `toml:"env_files" usage:"Paths to .env files to load before build/run"`
	Build       cfgBuild  `toml:"build"`
	Color       cfgColor  `toml:"color"`
	Log         cfgLog    `toml:"log"`
	Misc        cfgMisc   `toml:"misc"`
	Screen      cfgScreen `toml:"screen"`
	Proxy       cfgProxy  `toml:"proxy"`
}

type entrypoint []string

func (e *entrypoint) UnmarshalTOML(v interface{}) error {
	switch val := v.(type) {
	case nil:
		*e = nil
		return nil
	case string:
		*e = []string{val}
		return nil
	case []interface{}:
		values := make([]string, len(val))
		for i, raw := range val {
			s, ok := raw.(string)
			if !ok {
				return fmt.Errorf("entrypoint values must be strings, got %T", raw)
			}
			values[i] = s
		}
		*e = values
		return nil
	default:
		return fmt.Errorf("entrypoint must be a string or array of strings, got %T", v)
	}
}

func (e entrypoint) binary() string {
	if len(e) == 0 {
		return ""
	}
	return e[0]
}

func (e entrypoint) args() []string {
	if len(e) <= 1 {
		return nil
	}
	return e[1:]
}

type cfgBuildOverrides struct {
	CfgBuildCommon `toml:",inline"`
}

type CfgBuildCommon struct {
	PreCmd     []string   `toml:"pre_cmd" usage:"Array of commands to run before each build"`
	Cmd        string     `toml:"cmd" usage:"Just plain old shell command. You could use 'make' as well"`
	PostCmd    []string   `toml:"post_cmd" usage:"Array of commands to run after ^C"`
	Bin        string     `toml:"bin" usage:"Binary file yields from 'cmd', will be deprecated soon, recommend using entrypoint."`
	Entrypoint entrypoint `toml:"entrypoint" usage:"Binary file plus optional arguments relative to root, prefer [\"./tmp/main\", \"arg\"] form"`
	FullBin    string     `toml:"full_bin" usage:"Customize binary, can setup environment variables when run your app"`
	ArgsBin    []string   `toml:"args_bin" usage:"Add additional arguments when running binary (bin/full_bin)."`
}

type cfgBuild struct {
	CfgBuildCommon         `toml:",inline"`
	Log                    string             `toml:"log" usage:"This log file is placed in your tmp_dir"`
	IncludeExt             []string           `toml:"include_ext" usage:"Watch these filename extensions"`
	ExcludeDir             []string           `toml:"exclude_dir" usage:"Ignore these filename extensions or directories"`
	IncludeDir             []string           `toml:"include_dir" usage:"Watch these directories if you specified"`
	ExcludeFile            []string           `toml:"exclude_file" usage:"Exclude files"`
	IncludeFile            []string           `toml:"include_file" usage:"Watch these files"`
	ExcludeRegex           []string           `toml:"exclude_regex" usage:"Exclude specific regular expressions"`
	ExcludeUnchanged       bool               `toml:"exclude_unchanged" usage:"Exclude unchanged files"`
	IgnoreDangerousRootDir bool               `toml:"ignore_dangerous_root_dir" usage:"Ignore dangerous root directory that could cause excessive file watching"`
	FollowSymlink          bool               `toml:"follow_symlink" usage:"Follow symlink for directories"`
	Poll                   bool               `toml:"poll" usage:"Poll files for changes instead of using fsnotify"`
	PollInterval           int                `toml:"poll_interval" usage:"Poll interval (defaults to the minimum interval of 500ms)"`
	Delay                  int                `toml:"delay" usage:"It's not necessary to trigger build each time file changes if it's too frequent"`
	StopOnError            bool               `toml:"stop_on_error" usage:"Stop running old binary when build errors occur"`
	SendInterrupt          bool               `toml:"send_interrupt" usage:"Send Interrupt signal before killing process (windows does not support this feature)"`
	KillDelay              time.Duration      `toml:"kill_delay" usage:"Delay after sending Interrupt signal"`
	Rerun                  bool               `toml:"rerun" usage:"Rerun binary or not"`
	RerunDelay             int                `toml:"rerun_delay" usage:"Delay after each execution"`
	Windows                *cfgBuildOverrides `toml:"windows,omitempty"`
	Darwin                 *cfgBuildOverrides `toml:"darwin,omitempty"`
	Linux                  *cfgBuildOverrides `toml:"linux,omitempty"`
	regexCompiled          []*regexp.Regexp
	includeDirAbs          []string
	extraIncludeDirs       []string
}

func (c *cfgBuild) RegexCompiled() ([]*regexp.Regexp, error) {
	return c.regexCompiled, nil
}

func (c *cfgBuild) normalizeIncludeDirs(root string) {
	c.includeDirAbs = c.includeDirAbs[:0]
	c.extraIncludeDirs = c.extraIncludeDirs[:0]
	if root == "" {
		return
	}
	for _, dir := range c.IncludeDir {
		dir = cleanPath(dir)
		if dir == "" {
			continue
		}
		dir = filepath.Clean(dir)
		abs := dir
		if !filepath.IsAbs(dir) {
			abs = filepath.Join(root, dir)
		}
		abs = filepath.Clean(abs)
		if isSubPath(root, abs) {
			c.includeDirAbs = append(c.includeDirAbs, abs)
			continue
		}
		c.extraIncludeDirs = append(c.extraIncludeDirs, abs)
	}
}

type cfgLog struct {
	AddTime  bool `toml:"time" usage:"Show log time"`
	MainOnly bool `toml:"main_only" usage:"Only show main log (silences watcher, build, runner)"`
	Silent   bool `toml:"silent" usage:"silence all logs produced by air"`
}

type cfgColor struct {
	Main    string `toml:"main" usage:"Customize main part's color. If no color found, use the raw app log"`
	Watcher string `toml:"watcher" usage:"Customize watcher part's color"`
	Build   string `toml:"build" usage:"Customize build part's color"`
	Runner  string `toml:"runner" usage:"Customize runner part's color"`
	App     string `toml:"app"`
}

type cfgMisc struct {
	CleanOnExit   bool    `toml:"clean_on_exit" usage:"Delete tmp directory on exit"`
	StartupBanner *string `toml:"startup_banner" usage:"Custom startup banner text; set to empty string to hide banner"`
}

type cfgScreen struct {
	ClearOnRebuild bool `toml:"clear_on_rebuild" usage:"Clear screen on rebuild"`
	KeepScroll     bool `toml:"keep_scroll" usage:"Keep scroll position after rebuild"`
}

type cfgProxy struct {
	Enabled         bool `toml:"enabled" usage:"Enable live-reloading on the browser"`
	ProxyPort       int  `toml:"proxy_port" usage:"Port for proxy server"`
	AppPort         int  `toml:"app_port" usage:"Port for your app"`
	AppStartTimeout int  `toml:"app_start_timeout" usage:"Timeout for waiting for app to start in milliseconds (default 5000)"`
}

type sliceTransformer struct{}

func (t sliceTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
	if typ.Kind() == reflect.Slice {
		return func(dst, src reflect.Value) error {
			if !src.IsZero() {
				dst.Set(src)
			}
			return nil
		}
	}
	return nil
}

// InitConfig initializes the configuration.
func InitConfig(path string, cmdArgs map[string]TomlInfo) (*Config, error) {
	ret, err := initConfigWithoutPreprocess(path)
	if err != nil {
		return nil, err
	}

	err = ret.preprocess(cmdArgs)
	if err != nil {
		return nil, err
	}
	warnDeprecatedBin(ret)

	return ret, nil
}

// InitConfigForDisplay initializes config without preprocess side effects.
func InitConfigForDisplay(path string, cmdArgs map[string]TomlInfo) (*Config, error) {
	ret, err := initConfigWithoutPreprocess(path)
	if err != nil {
		return nil, err
	}
	if cmdArgs != nil {
		ret.withArgs(cmdArgs)
	}
	return ret, nil
}

func initConfigWithoutPreprocess(path string) (*Config, error) {
	var (
		cfg *Config
		err error
	)

	if path == "" {
		cfg, err = defaultPathConfig()
		if err != nil {
			return nil, err
		}
	} else {
		cfg, err = readConfigOrDefault(path)
		if err != nil {
			return nil, err
		}
	}
	config := defaultConfig()
	// get addr
	ret := &config
	err = mergo.Merge(ret, cfg, func(config *mergo.Config) {
		// mergo.Merge will overwrite the fields if it is Empty
		// So need use this to avoid that none-zero slice will be overwritten.
		// https://dario.cat/mergo#transformers
		config.Transformers = sliceTransformer{}
		config.Overwrite = true
	})
	if err != nil {
		return nil, err
	}

	if err = applyPlatformOverrides(ret); err != nil {
		return nil, err
	}
	return ret, nil
}

func writeDefaultConfig() (string, error) {
	fstat, err := os.Stat(dftTOML)
	if err != nil && !os.IsNotExist(err) {
		return "", fmt.Errorf("failed to check for existing configuration: %w", err)
	}
	if err == nil && fstat != nil {
		return "", errors.New("configuration already exists")
	}

	file, err := os.Create(dftTOML)
	if err != nil {
		return "", fmt.Errorf("failed to create a new configuration: %w", err)
	}
	defer file.Close()

	config := defaultConfigBase()
	setEntrypointFromBin(&config)
	addPlatformOverridesForInit(&config, runtime.GOOS)
	configFile, err := toml.Marshal(config)
	if err != nil {
		return "", fmt.Errorf("failed to marshal the default configuration: %w", err)
	}

	headers := []byte(schemaHeader + "\n\n")
	content := append(headers, configFile...)

	_, err = file.Write(content)
	if err != nil {
		return "", fmt.Errorf("failed to write to %s: %w", dftTOML, err)
	}

	return dftTOML, nil
}

func defaultPathConfig() (*Config, error) {
	// when path is blank, first find `.air.toml` in `air_wd` and current working directory, if not found, use defaults
	cfg, err := readConfByName(dftTOML)
	if err == nil {
		return cfg, nil
	}

	// If the config file exists but failed to parse, report the error
	// Only use defaults if no config file exists
	if !os.IsNotExist(err) {
		return nil, fmt.Errorf("failed to parse %s: %w", dftTOML, err)
	}

	dftCfg := defaultConfig()
	setEntrypointFromBin(&dftCfg)
	return &dftCfg, nil
}

func readConfByName(name string) (*Config, error) {
	var path string
	if wd := os.Getenv(airWd); wd != "" {
		path = filepath.Join(wd, name)
	} else {
		wd, err := os.Getwd()
		if err != nil {
			return nil, err
		}
		path = filepath.Join(wd, name)
	}
	cfg, err := readConfig(path)
	return cfg, err
}

func defaultConfigBase() Config {
	build := cfgBuild{
		CfgBuildCommon: CfgBuildCommon{
			Cmd:        "go build -o ./tmp/main .",
			Bin:        "./tmp/main",
			Entrypoint: entrypoint{},
			PreCmd:     []string{},
			PostCmd:    []string{},
			ArgsBin:    []string{},
		},
		Log:          "build-errors.log",
		IncludeExt:   []string{"go", "tpl", "tmpl", "html"},
		IncludeDir:   []string{},
		ExcludeFile:  []string{},
		IncludeFile:  []string{},
		ExcludeDir:   []string{"assets", "tmp", "vendor", "testdata"},
		ExcludeRegex: []string{"_test.go"},
		Delay:        1000,
		Rerun:        false,
		RerunDelay:   500,
	}
	log := cfgLog{
		AddTime:  false,
		MainOnly: false,
		Silent:   false,
	}
	color := cfgColor{
		Main:    "magenta",
		Watcher: "cyan",
		Build:   "yellow",
		Runner:  "green",
	}
	misc := cfgMisc{
		CleanOnExit: false,
	}
	return Config{
		Root:        ".",
		TmpDir:      "tmp",
		TestDataDir: "testdata",
		EnvFiles:    []string{},
		Build:       build,
		Color:       color,
		Log:         log,
		Misc:        misc,
		Screen: cfgScreen{
			ClearOnRebuild: false,
			KeepScroll:     true,
		},
	}
}

func defaultConfigForOS(goos string) Config {
	cfg := defaultConfigBase()
	applyPlatformDefaults(&cfg, goos)
	return cfg
}

func defaultConfig() Config {
	return defaultConfigForOS(runtime.GOOS)
}

func applyPlatformDefaults(cfg *Config, goos string) {
	if cfg == nil {
		return
	}
	if goos == PlatformWindows {
		cfg.Build.Bin = `tmp\main.exe`
		cfg.Build.Cmd = "go build -o ./tmp/main.exe ."
	}
}

func applyPlatformOverrides(cfg *Config) error {
	if cfg == nil {
		return nil
	}

	override := platformBuildOverrides(&cfg.Build, runtime.GOOS)
	if override == nil {
		return nil
	}

	applyBuildOverrides(&cfg.Build, override)
	return nil
}

func platformBuildOverrides(build *cfgBuild, goos string) *cfgBuildOverrides {
	if build == nil {
		return nil
	}
	switch goos {
	case PlatformWindows:
		return build.Windows
	case "darwin":
		return build.Darwin
	case "linux":
		return build.Linux
	default:
		return nil
	}
}

func applyBuildOverrides(build *cfgBuild, override *cfgBuildOverrides) {
	if build == nil || override == nil {
		return
	}
	if override.PreCmd != nil {
		build.PreCmd = override.PreCmd
	}
	if override.Cmd != "" {
		build.Cmd = override.Cmd
	}
	if override.PostCmd != nil {
		build.PostCmd = override.PostCmd
	}
	if override.Bin != "" {
		build.Bin = override.Bin
	}
	if override.Entrypoint != nil {
		build.Entrypoint = override.Entrypoint
	}
	if override.FullBin != "" {
		build.FullBin = override.FullBin
	}
	if override.ArgsBin != nil {
		build.ArgsBin = override.ArgsBin
	}
}

func setEntrypointFromBin(cfg *Config) {
	if cfg == nil {
		return
	}
	if len(cfg.Build.Entrypoint) == 0 && cfg.Build.Bin != "" {
		cfg.Build.Entrypoint = entrypoint{cfg.Build.Bin}
	}
}

func addPlatformOverridesForInit(cfg *Config, goos string) {
	if cfg == nil {
		return
	}
	platformConfig := defaultConfigForOS(goos)
	setEntrypointFromBin(&platformConfig)
	override := buildOverridesFromDiff(cfg.Build, platformConfig.Build)
	if override == nil {
		return
	}
	switch goos {
	case PlatformWindows:
		cfg.Build.Windows = override
	case "darwin":
		cfg.Build.Darwin = override
	case "linux":
		cfg.Build.Linux = override
	}
}

func buildOverridesFromDiff(base cfgBuild, target cfgBuild) *cfgBuildOverrides {
	var override cfgBuildOverrides
	changed := false

	if !reflect.DeepEqual(base.PreCmd, target.PreCmd) {
		override.PreCmd = target.PreCmd
		changed = true
	}
	if base.Cmd != target.Cmd {
		override.Cmd = target.Cmd
		changed = true
	}
	if !reflect.DeepEqual(base.PostCmd, target.PostCmd) {
		override.PostCmd = target.PostCmd
		changed = true
	}
	if base.Bin != target.Bin {
		override.Bin = target.Bin
		changed = true
	}
	if !reflect.DeepEqual(base.Entrypoint, target.Entrypoint) {
		override.Entrypoint = target.Entrypoint
		changed = true
	}
	if base.FullBin != target.FullBin {
		override.FullBin = target.FullBin
		changed = true
	}
	if !reflect.DeepEqual(base.ArgsBin, target.ArgsBin) {
		override.ArgsBin = target.ArgsBin
		changed = true
	}

	if !changed {
		return nil
	}
	return &override
}

func readConfig(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	cfg := new(Config)
	if err = toml.Unmarshal(data, cfg); err != nil {
		return nil, err
	}

	return cfg, nil
}

func readConfigOrDefault(path string) (*Config, error) {
	dftCfg := defaultConfig()
	cfg, err := readConfig(path)
	if err != nil {
		return &dftCfg, err
	}

	return cfg, nil
}

func (c *Config) preprocess(args map[string]TomlInfo) error {
	var err error

	if args != nil {
		c.withArgs(args)
	}
	cwd := os.Getenv(airWd)
	if cwd != "" {
		if err = os.Chdir(cwd); err != nil {
			return err
		}
		c.Root = cwd
	}
	c.Root, err = expandPath(c.Root)
	if err != nil {
		return err
	}

	// Check for dangerous root directories that could cause excessive file watching
	if isDangerous, dirName := isDangerousRoot(c.Root); isDangerous {
		if !c.Build.IgnoreDangerousRootDir {
			return fmt.Errorf("refusing to run in %s - this would watch too many files. Please run air in a project directory", dirName)
		}
		fmt.Fprintln(os.Stderr, "[warning] ignoring root directory protections. This could cause excessive file watching. It is recommended to run air in a project directory")
	}

	if c.TmpDir == "" {
		c.TmpDir = "tmp"
	}
	if c.TestDataDir == "" {
		c.TestDataDir = "testdata"
	}
	c.adjustDefaultsForTmpDir()
	ed := c.Build.ExcludeDir
	for i := range ed {
		ed[i] = cleanPath(ed[i])
	}

	if len(c.Build.Entrypoint) > 0 {
		entry := c.Build.Entrypoint.binary()
		if !filepath.IsAbs(entry) {
			if resolved := resolveCommandPath(entry); resolved != "" {
				entry = resolved
			} else {
				entry = joinPath(c.Root, entry)
			}
		}

		entry, err = filepath.Abs(entry)
		if err != nil {
			return err
		}
		c.Build.Entrypoint[0] = entry
	}

	adaptToVariousPlatforms(c)
	c.Build.normalizeIncludeDirs(c.Root)

	// Join runtime arguments with the configuration arguments
	runtimeArgs := flag.Args()
	c.Build.ArgsBin = append(c.Build.ArgsBin, runtimeArgs...)

	// Compile the exclude regexes if there are any patterns in the config file
	if len(c.Build.ExcludeRegex) > 0 {
		regexCompiled := make([]*regexp.Regexp, len(c.Build.ExcludeRegex))
		for idx, expr := range c.Build.ExcludeRegex {
			re, err := regexp.Compile(expr)
			if err != nil {
				return fmt.Errorf("failed to compile regex %q: %w", expr, err)
			}
			regexCompiled[idx] = re
		}
		c.Build.regexCompiled = regexCompiled
	}

	c.Build.ExcludeDir = ed
	if len(c.Build.FullBin) > 0 {
		c.Build.Bin = c.Build.FullBin
		return err
	}
	// Fix windows CMD processor
	// CMD will not recognize relative path like ./tmp/server
	c.Build.Bin, err = filepath.Abs(c.Build.Bin)

	return err
}

// adjustDefaultsForTmpDir updates Build.Cmd, Build.Bin, and Build.ExcludeDir
// when they still hold their default values but TmpDir has been changed.
func (c *Config) adjustDefaultsForTmpDir() {
	c.adjustDefaultsForTmpDirWithOS(runtime.GOOS)
}

func (c *Config) adjustDefaultsForTmpDirWithOS(goos string) {
	const defaultTmpDir = "tmp"
	if c.TmpDir == defaultTmpDir {
		return
	}

	defaultCmd := "go build -o ./tmp/main ."
	defaultBin := "./tmp/main"
	mainBinary := "main"
	if goos == PlatformWindows {
		defaultCmd = "go build -o ./tmp/main.exe ."
		defaultBin = `tmp\main.exe`
		mainBinary = "main.exe"
	}

	newBinPath := filepath.Join(c.TmpDir, mainBinary)
	normalizedBinPath := strings.ReplaceAll(newBinPath, `\`, "/")
	newBin := "./" + normalizedBinPath
	cmdOut := newBin
	if isAbsPathForOS(goos, c.TmpDir) {
		newBin = newBinPath
		cmdOut = normalizedBinPath
	}
	if goos == PlatformWindows {
		if isAbsPathForOS(goos, c.TmpDir) {
			newBin = strings.ReplaceAll(newBinPath, "/", "\\")
			cmdOut = normalizedBinPath
		} else {
			newBin = strings.ReplaceAll(newBinPath, "/", "\\")
			cmdOut = "./" + normalizedBinPath
		}
	}
	newCmd := "go build -o " + cmdOut + " ."

	if c.Build.Cmd == defaultCmd {
		c.Build.Cmd = newCmd
	}
	if c.Build.Bin == defaultBin {
		c.Build.Bin = newBin
	}
	if isDefaultExcludeDir(c.Build.ExcludeDir) {
		for i, dir := range c.Build.ExcludeDir {
			if dir == defaultTmpDir {
				c.Build.ExcludeDir[i] = c.TmpDir
			}
		}
	}
}

func isDefaultExcludeDir(dirs []string) bool {
	defaultDirs := []string{"assets", "tmp", "vendor", "testdata"}
	if len(dirs) != len(defaultDirs) {
		return false
	}
	for i := range dirs {
		if dirs[i] != defaultDirs[i] {
			return false
		}
	}
	return true
}

func isAbsPathForOS(goos, path string) bool {
	if goos != PlatformWindows {
		return pathpkg.IsAbs(path)
	}

	if strings.HasPrefix(path, `\\`) {
		return true
	}
	if len(path) < 3 {
		return false
	}
	drive := path[0]
	if ((drive >= 'a' && drive <= 'z') || (drive >= 'A' && drive <= 'Z')) && path[1] == ':' {
		return path[2] == '\\' || path[2] == '/'
	}
	return false
}

func (c *Config) colorInfo() map[string]string {
	return map[string]string{
		"main":    c.Color.Main,
		"build":   c.Color.Build,
		"runner":  c.Color.Runner,
		"watcher": c.Color.Watcher,
	}
}

func (c *Config) buildLogPath() string {
	return joinPath(c.tmpPath(), c.Build.Log)
}

func (c *Config) buildDelay() time.Duration {
	return time.Duration(c.Build.Delay) * time.Millisecond
}

func (c *Config) rerunDelay() time.Duration {
	return time.Duration(c.Build.RerunDelay) * time.Millisecond
}

func (c *Config) killDelay() time.Duration {
	// kill_delay can be specified as an integer or duration string
	// interpret as milliseconds if less than the value of 1 millisecond
	if c.Build.KillDelay < time.Millisecond {
		return c.Build.KillDelay * time.Millisecond
	}
	// normalize kill delay to milliseconds
	return time.Duration(c.Build.KillDelay.Milliseconds()) * time.Millisecond
}

func (c *Config) binPath() string {
	if len(c.Build.Entrypoint) > 0 {
		return c.Build.Entrypoint.binary()
	}
	return joinPath(c.Root, c.Build.Bin)
}

func (c *Config) runnerBin() string {
	if len(c.Build.Entrypoint) > 0 && len(c.Build.FullBin) == 0 {
		return c.Build.Entrypoint.binary()
	}
	return c.Build.Bin
}

func (c *Config) tmpPath() string {
	return joinPath(c.Root, c.TmpDir)
}

func (c *Config) testDataPath() string {
	return joinPath(c.Root, c.TestDataDir)
}

func (c *Config) rel(path string) string {
	s, err := filepath.Rel(c.Root, path)
	if err != nil {
		return ""
	}
	return s
}

func resolveCommandPath(entry string) string {
	if entry == "" || strings.ContainsAny(entry, `/\`) {
		return ""
	}

	path, err := exec.LookPath(entry)
	if err != nil {
		return ""
	}
	return path
}

// withArgs returns a new config with the given arguments added to the configuration.
func (c *Config) withArgs(args map[string]TomlInfo) {
	for _, value := range args {
		// Ignore values that match the default configuration.
		// This ensures user-specified configurations are not overwritten by default values.
		if value.Value != nil && *value.Value != value.fieldValue {
			v := reflect.ValueOf(c)
			setValue2Struct(v, value.fieldPath, *value.Value)
		}
	}

}

func warnDeprecatedBin(cfg *Config) {
	if cfg == nil {
		return
	}
	if cfg.Build.Bin == "" || len(cfg.Build.Entrypoint) > 0 {
		return
	}

	if cfg.Build.Bin != "" && len(cfg.Build.Entrypoint) > 0 {
		return
	}

	fmt.Fprintln(os.Stderr, "[warning] build.bin is deprecated; set build.entrypoint instead")
}
