// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package scan

import (
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"strings"

	"golang.org/x/tools/go/buildutil"
	"golang.org/x/vuln/internal/govulncheck"
)

type config struct {
	govulncheck.Config
	patterns []string
	db       string
	dir      string
	tags     buildutil.TagsFlag
	test     bool
	show     ShowFlag
	format   FormatFlag
	env      []string
}

func parseFlags(cfg *config, stderr io.Writer, args []string) error {
	var version bool
	var json bool
	var scanFlag ScanFlag
	var modeFlag ModeFlag
	flags := flag.NewFlagSet("", flag.ContinueOnError)
	flags.SetOutput(stderr)
	flags.BoolVar(&json, "json", false, "output JSON (Go compatible legacy flag, see format flag)")
	flags.BoolVar(&cfg.test, "test", false, "analyze test files (only valid for source mode, default false)")
	flags.StringVar(&cfg.dir, "C", "", "change to `dir` before running govulncheck")
	flags.StringVar(&cfg.db, "db", "https://vuln.go.dev", "vulnerability database `url`")
	flags.Var(&modeFlag, "mode", "supports 'source', 'binary', and 'extract' (default 'source')")
	flags.Var(&cfg.tags, "tags", "comma-separated `list` of build tags")
	flags.Var(&cfg.show, "show", "enable display of additional information specified by the comma separated `list`\nThe supported values are 'traces','color', 'version', and 'verbose'")
	flags.Var(&cfg.format, "format", "specify format output\nThe supported values are 'text', 'json', 'sarif', and 'openvex' (default 'text')")
	flags.BoolVar(&version, "version", false, "print the version information")
	flags.Var(&scanFlag, "scan", "set the scanning level desired, one of 'module', 'package', or 'symbol' (default 'symbol')")

	// We don't want to print the whole usage message on each flags
	// error, so we set to a no-op and do the printing ourselves.
	flags.Usage = func() {}
	usage := func() {
		fmt.Fprint(flags.Output(), `Govulncheck reports known vulnerabilities in dependencies.

Usage:

	govulncheck [flags] [patterns]
	govulncheck -mode=binary [flags] [binary]

`)
		flags.PrintDefaults()
		fmt.Fprintf(flags.Output(), "\n%s\n", detailsMessage)
	}

	if err := flags.Parse(args); err != nil {
		if err == flag.ErrHelp {
			usage() // print usage only on help
			return errHelp
		}
		return errUsage
	}
	cfg.patterns = flags.Args()
	if version {
		cfg.show = append(cfg.show, "version")
	}
	cfg.ScanLevel = govulncheck.ScanLevel(scanFlag)
	cfg.ScanMode = govulncheck.ScanMode(modeFlag)
	if err := validateConfig(cfg, json); err != nil {
		fmt.Fprintln(flags.Output(), err)
		return errUsage
	}
	return nil
}

func validateConfig(cfg *config, json bool) error {
	// take care of default values
	if cfg.ScanMode == "" {
		cfg.ScanMode = govulncheck.ScanModeSource
	}
	if cfg.ScanLevel == "" {
		cfg.ScanLevel = govulncheck.ScanLevelSymbol
	}
	if json {
		if cfg.format != formatUnset {
			return fmt.Errorf("the -json flag cannot be used with -format flag")
		}
		cfg.format = formatJSON
	} else {
		if cfg.format == formatUnset {
			cfg.format = formatText
		}
	}

	// show flag is only supported with text output
	if cfg.format != formatText && len(cfg.show) > 0 {
		return fmt.Errorf("the -show flag is not supported for %s output", cfg.format)
	}

	switch cfg.ScanMode {
	case govulncheck.ScanModeSource:
		if len(cfg.patterns) == 1 && isFile(cfg.patterns[0]) {
			return fmt.Errorf("%q is a file.\n\n%v", cfg.patterns[0], errNoBinaryFlag)
		}
		if cfg.ScanLevel == govulncheck.ScanLevelModule && len(cfg.patterns) != 0 {
			return fmt.Errorf("patterns are not accepted for module only scanning")
		}
	case govulncheck.ScanModeBinary:
		if cfg.test {
			return fmt.Errorf("the -test flag is not supported in binary mode")
		}
		if len(cfg.tags) > 0 {
			return fmt.Errorf("the -tags flag is not supported in binary mode")
		}
		if len(cfg.patterns) != 1 {
			return fmt.Errorf("only 1 binary can be analyzed at a time")
		}
		if !isFile(cfg.patterns[0]) {
			return fmt.Errorf("%q is not a file", cfg.patterns[0])
		}
	case govulncheck.ScanModeExtract:
		if cfg.test {
			return fmt.Errorf("the -test flag is not supported in extract mode")
		}
		if len(cfg.tags) > 0 {
			return fmt.Errorf("the -tags flag is not supported in extract mode")
		}
		if len(cfg.patterns) != 1 {
			return fmt.Errorf("only 1 binary can be extracted at a time")
		}
		if cfg.format == formatJSON {
			return fmt.Errorf("the json format must be off in extract mode")
		}
		if !isFile(cfg.patterns[0]) {
			return fmt.Errorf("%q is not a file (source extraction is not supported)", cfg.patterns[0])
		}
	case govulncheck.ScanModeConvert:
		if len(cfg.patterns) != 0 {
			return fmt.Errorf("patterns are not accepted in convert mode")
		}
		if cfg.dir != "" {
			return fmt.Errorf("the -C flag is not supported in convert mode")
		}
		if cfg.test {
			return fmt.Errorf("the -test flag is not supported in convert mode")
		}
		if len(cfg.tags) > 0 {
			return fmt.Errorf("the -tags flag is not supported in convert mode")
		}
	case govulncheck.ScanModeQuery:
		if cfg.test {
			return fmt.Errorf("the -test flag is not supported in query mode")
		}
		if len(cfg.tags) > 0 {
			return fmt.Errorf("the -tags flag is not supported in query mode")
		}
		if cfg.format != formatJSON {
			return fmt.Errorf("the json format must be set in query mode")
		}
		for _, pattern := range cfg.patterns {
			// Parse the input here so that we can catch errors before
			// outputting the Config.
			if _, _, err := parseModuleQuery(pattern); err != nil {
				return err
			}
		}
	}
	return nil
}

func isFile(path string) bool {
	s, err := os.Stat(path)
	if err != nil {
		return false
	}
	return !s.IsDir()
}

var errFlagParse = errors.New("see -help for details")

// ShowFlag is used for parsing and validation of
// govulncheck -show flag.
type ShowFlag []string

var supportedShows = map[string]bool{
	"traces":  true,
	"color":   true,
	"verbose": true,
	"version": true,
}

func (v *ShowFlag) Set(s string) error {
	if s == "" {
		return nil
	}
	for _, show := range strings.Split(s, ",") {
		sh := strings.TrimSpace(show)
		if _, ok := supportedShows[sh]; !ok {
			return errFlagParse
		}
		*v = append(*v, sh)
	}
	return nil
}

func (v *ShowFlag) Get() interface{} { return *v }
func (v *ShowFlag) String() string   { return "" }

// Update the text handler h with values of the flag.
func (v ShowFlag) Update(h *TextHandler) {
	for _, show := range v {
		switch show {
		case "traces":
			h.showTraces = true
		case "color":
			h.showColor = true
		case "version":
			h.showVersion = true
		case "verbose":
			h.showVerbose = true
		}
	}
}

// FormatFlag is used for parsing and validation of
// govulncheck -format flag.
type FormatFlag string

const (
	formatUnset   = ""
	formatJSON    = "json"
	formatText    = "text"
	formatSarif   = "sarif"
	formatOpenVEX = "openvex"
)

var supportedFormats = map[string]bool{
	formatJSON:    true,
	formatText:    true,
	formatSarif:   true,
	formatOpenVEX: true,
}

func (f *FormatFlag) Get() interface{} { return *f }
func (f *FormatFlag) Set(s string) error {
	if _, ok := supportedFormats[s]; !ok {
		return errFlagParse
	}
	*f = FormatFlag(s)
	return nil
}
func (f *FormatFlag) String() string { return "" }

// ModeFlag is used for parsing and validation of
// govulncheck -mode flag.
type ModeFlag string

var supportedModes = map[string]bool{
	govulncheck.ScanModeSource:  true,
	govulncheck.ScanModeBinary:  true,
	govulncheck.ScanModeConvert: true,
	govulncheck.ScanModeQuery:   true,
	govulncheck.ScanModeExtract: true,
}

func (f *ModeFlag) Get() interface{} { return *f }
func (f *ModeFlag) Set(s string) error {
	if _, ok := supportedModes[s]; !ok {
		return errFlagParse
	}
	*f = ModeFlag(s)
	return nil
}
func (f *ModeFlag) String() string { return "" }

// ScanFlag is used for parsing and validation of
// govulncheck -scan flag.
type ScanFlag string

var supportedLevels = map[string]bool{
	govulncheck.ScanLevelModule:  true,
	govulncheck.ScanLevelPackage: true,
	govulncheck.ScanLevelSymbol:  true,
}

func (f *ScanFlag) Get() interface{} { return *f }
func (f *ScanFlag) Set(s string) error {
	if _, ok := supportedLevels[s]; !ok {
		return errFlagParse
	}
	*f = ScanFlag(s)
	return nil
}
func (f *ScanFlag) String() string { return "" }
