package config

import (
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/genuinetools/reg/registry"
	"github.com/urfave/cli/v2"
	"go.uber.org/zap"
	"golang.org/x/xerrors"

	dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
	"github.com/aquasecurity/trivy/pkg/log"
)

type Config struct {
	context *cli.Context
	logger  *zap.SugaredLogger

	Quiet bool
	Debug bool

	CacheDir   string
	ClearCache bool

	Input    string
	output   string
	Format   string
	Template string

	Timeout         time.Duration
	ScanRemovedPkgs bool
	vulnType        string
	severities      string
	IgnoreFile      string
	IgnoreUnfixed   bool
	ExitCode        int

	RemoteAddr    string
	token         string
	tokenHeader   string
	customHeaders []string
	CustomHeaders http.Header

	// these variables are generated by Init()
	ImageName  string
	VulnType   []string
	Output     *os.File
	Severities []dbTypes.Severity
	AppVersion string
}

func New(c *cli.Context) (Config, error) {
	debug := c.Bool("debug")
	quiet := c.Bool("quiet")
	logger, err := log.NewLogger(debug, quiet)
	if err != nil {
		return Config{}, xerrors.New("failed to create a logger")
	}
	return Config{
		context: c,
		logger:  logger,

		Quiet: quiet,
		Debug: debug,

		CacheDir:   c.String("cache-dir"),
		ClearCache: c.Bool("clear-cache"),

		Input:    c.String("input"),
		output:   c.String("output"),
		Format:   c.String("format"),
		Template: c.String("template"),

		Timeout:         c.Duration("timeout"),
		ScanRemovedPkgs: c.Bool("removed-pkgs"),
		vulnType:        c.String("vuln-type"),
		severities:      c.String("severity"),
		IgnoreFile:      c.String("ignorefile"),
		IgnoreUnfixed:   c.Bool("ignore-unfixed"),
		ExitCode:        c.Int("exit-code"),

		RemoteAddr:    c.String("remote"),
		token:         c.String("token"),
		tokenHeader:   c.String("token-header"),
		customHeaders: c.StringSlice("custom-headers"),
	}, nil
}

func (c *Config) Init() (err error) {
	c.Severities = c.splitSeverity(c.severities)
	c.VulnType = strings.Split(c.vulnType, ",")
	c.AppVersion = c.context.App.Version
	c.CustomHeaders = splitCustomHeaders(c.customHeaders)

	// add token to custom headers
	if c.token != "" {
		c.CustomHeaders.Set(c.tokenHeader, c.token)
	}

	// --clear-cache doesn't conduct the scan
	if c.ClearCache {
		return nil
	}

	ctx := c.context
	if c.Input == "" && ctx.NArg() == 0 {
		c.logger.Error(`trivy requires at least 1 argument or --input option`)
		cli.ShowAppHelp(c.context)
		return xerrors.New("arguments error")
	} else if ctx.NArg() > 1 {
		c.logger.Error(`multiple images cannot be specified`)
		return xerrors.New("arguments error")
	}

	c.Output = os.Stdout
	if c.output != "" {
		if c.Output, err = os.Create(c.output); err != nil {
			return xerrors.Errorf("failed to create an output file: %w", err)
		}
	}

	if c.Input == "" {
		c.ImageName = ctx.Args().First()
	}

	// Check whether 'latest' tag is used
	if c.ImageName != "" {
		image, err := registry.ParseImage(c.ImageName)
		if err != nil {
			return xerrors.Errorf("invalid image: %w", err)
		}
		if image.Tag == "latest" {
			c.logger.Warn("You should avoid using the :latest tag as it is cached. You need to specify '--clear-cache' option when :latest image is changed")
		}
	}

	return nil
}

func (c *Config) splitSeverity(severity string) []dbTypes.Severity {
	c.logger.Debugf("Severities: %s", severity)
	var severities []dbTypes.Severity
	for _, s := range strings.Split(severity, ",") {
		severity, err := dbTypes.NewSeverity(s)
		if err != nil {
			c.logger.Warnf("unknown severity option: %s", err)
		}
		severities = append(severities, severity)
	}
	return severities
}

func splitCustomHeaders(headers []string) http.Header {
	result := make(http.Header)
	for _, header := range headers {
		// e.g. x-api-token:XXX
		s := strings.SplitN(header, ":", 2)
		if len(s) != 2 {
			continue
		}
		result.Set(s[0], s[1])
	}
	return result
}
