package flag

import (
	"path/filepath"
	"slices"
	"strings"

	"github.com/mattn/go-shellwords"
	"github.com/spf13/viper"
	"golang.org/x/xerrors"

	dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
	"github.com/aquasecurity/trivy/pkg/cache"
	"github.com/aquasecurity/trivy/pkg/compliance/spec"
	"github.com/aquasecurity/trivy/pkg/log"
	"github.com/aquasecurity/trivy/pkg/result"
	"github.com/aquasecurity/trivy/pkg/types"
	"github.com/aquasecurity/trivy/pkg/utils/fsutils"
	xslices "github.com/aquasecurity/trivy/pkg/x/slices"
	xstrings "github.com/aquasecurity/trivy/pkg/x/strings"
)

// e.g. config yaml:
//
//	format: table
//	dependency-tree: true
//	severity: HIGH,CRITICAL
var (
	FormatFlag = Flag[string]{
		Name:          "format",
		ConfigName:    "format",
		Shorthand:     "f",
		Default:       string(types.FormatTable),
		Values:        xstrings.ToStringSlice(types.SupportedFormats),
		Usage:         "format",
		TelemetrySafe: true,
	}
	ReportFormatFlag = Flag[string]{
		Name:       "report",
		ConfigName: "report",
		Default:    "all",
		Values: []string{
			"all",
			"summary",
		},
		Usage:         "specify a report format for the output",
		TelemetrySafe: true,
	}
	TemplateFlag = Flag[string]{
		Name:       "template",
		ConfigName: "template",
		Shorthand:  "t",
		Usage:      "output template (file path must have .tpl extension)",
	}
	DependencyTreeFlag = Flag[bool]{
		Name:          "dependency-tree",
		ConfigName:    "dependency-tree",
		Usage:         "[EXPERIMENTAL] show dependency origin tree of vulnerable packages",
		TelemetrySafe: true,
	}
	ListAllPkgsFlag = Flag[bool]{
		Name:          "list-all-pkgs",
		ConfigName:    "list-all-pkgs",
		Default:       true,
		Usage:         "output all packages in the JSON report regardless of vulnerability",
		TelemetrySafe: true,
	}
	IgnoreFileFlag = Flag[string]{
		Name:       "ignorefile",
		ConfigName: "ignorefile",
		Default:    result.DefaultIgnoreFile,
		Usage:      "specify .trivyignore file",
	}
	IgnorePolicyFlag = Flag[string]{
		Name:       "ignore-policy",
		ConfigName: "ignore-policy",
		Usage:      "specify the Rego file path to evaluate each vulnerability",
	}
	ExitCodeFlag = Flag[int]{
		Name:          "exit-code",
		ConfigName:    "exit-code",
		Usage:         "specify exit code when any security issues are found",
		TelemetrySafe: true,
	}
	ExitOnEOLFlag = Flag[int]{
		Name:          "exit-on-eol",
		ConfigName:    "exit-on-eol",
		Usage:         "exit with the specified code when the OS reaches end of service/life",
		TelemetrySafe: true,
	}
	OutputFlag = Flag[string]{
		Name:       "output",
		ConfigName: "output",
		Shorthand:  "o",
		Usage:      "output file name",
	}
	OutputPluginArgFlag = Flag[string]{
		Name:       "output-plugin-arg",
		ConfigName: "output-plugin-arg",
		Usage:      "[EXPERIMENTAL] output plugin arguments",
	}
	SeverityFlag = Flag[[]string]{
		Name:          "severity",
		ConfigName:    "severity",
		Shorthand:     "s",
		Default:       dbTypes.SeverityNames,
		Values:        dbTypes.SeverityNames,
		Usage:         "severities of security issues to be displayed",
		TelemetrySafe: true,
	}
	ComplianceFlag = Flag[string]{
		Name:       "compliance",
		ConfigName: "scan.compliance",
		Usage:      "compliance report to generate",
	}
	ShowSuppressedFlag = Flag[bool]{
		Name:          "show-suppressed",
		ConfigName:    "scan.show-suppressed",
		Usage:         "[EXPERIMENTAL] show suppressed vulnerabilities",
		TelemetrySafe: true,
	}
	TableModeFlag = Flag[[]string]{
		Name:       "table-mode",
		ConfigName: "table-mode",
		Default:    xstrings.ToStringSlice(types.SupportedTableModes),
		Values:     xstrings.ToStringSlice(types.SupportedTableModes),
		Usage:      "[EXPERIMENTAL] tables that will be displayed in 'table' format",
	}
)

// ReportFlagGroup composes common printer flag structs
// used for commands requiring reporting logic.
type ReportFlagGroup struct {
	Format          *Flag[string]
	ReportFormat    *Flag[string]
	Template        *Flag[string]
	DependencyTree  *Flag[bool]
	ListAllPkgs     *Flag[bool]
	IgnoreFile      *Flag[string]
	IgnorePolicy    *Flag[string]
	ExitCode        *Flag[int]
	ExitOnEOL       *Flag[int]
	Output          *Flag[string]
	OutputPluginArg *Flag[string]
	Severity        *Flag[[]string]
	Compliance      *Flag[string]
	ShowSuppressed  *Flag[bool]
	TableMode       *Flag[[]string]
}

type ReportOptions struct {
	Format           types.Format
	ReportFormat     string
	Template         string
	DependencyTree   bool
	ListAllPkgs      bool
	IgnoreFile       string
	ExitCode         int
	ExitOnEOL        int
	IgnorePolicy     string
	Output           string
	OutputPluginArgs []string
	Severities       []dbTypes.Severity
	Compliance       spec.ComplianceSpec
	ShowSuppressed   bool
	TableModes       []types.TableMode
}

func NewReportFlagGroup() *ReportFlagGroup {
	return &ReportFlagGroup{
		Format:          FormatFlag.Clone(),
		ReportFormat:    ReportFormatFlag.Clone(),
		Template:        TemplateFlag.Clone(),
		DependencyTree:  DependencyTreeFlag.Clone(),
		ListAllPkgs:     ListAllPkgsFlag.Clone(),
		IgnoreFile:      IgnoreFileFlag.Clone(),
		IgnorePolicy:    IgnorePolicyFlag.Clone(),
		ExitCode:        ExitCodeFlag.Clone(),
		ExitOnEOL:       ExitOnEOLFlag.Clone(),
		Output:          OutputFlag.Clone(),
		OutputPluginArg: OutputPluginArgFlag.Clone(),
		Severity:        SeverityFlag.Clone(),
		Compliance:      ComplianceFlag.Clone(),
		ShowSuppressed:  ShowSuppressedFlag.Clone(),
		TableMode:       TableModeFlag.Clone(),
	}
}

func (f *ReportFlagGroup) Name() string {
	return "Report"
}

func (f *ReportFlagGroup) Flags() []Flagger {
	return []Flagger{
		f.Format,
		f.ReportFormat,
		f.Template,
		f.DependencyTree,
		f.ListAllPkgs,
		f.IgnoreFile,
		f.IgnorePolicy,
		f.ExitCode,
		f.ExitOnEOL,
		f.Output,
		f.OutputPluginArg,
		f.Severity,
		f.Compliance,
		f.ShowSuppressed,
		f.TableMode,
	}
}

func (f *ReportFlagGroup) ToOptions(opts *Options) error {
	format := types.Format(f.Format.Value())
	template := f.Template.Value()
	dependencyTree := f.DependencyTree.Value()
	listAllPkgs := f.ListAllPkgs.Value()
	tableModes := f.TableMode.Value()

	if template != "" {
		switch format {
		case "":
			log.Warn("'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.")
		case "template":
			// Validate template file extension
			if path, ok := strings.CutPrefix(template, "@"); ok {
				if filepath.Ext(path) != ".tpl" {
					return xerrors.Errorf("template file must have .tpl extension: %s", path)
				}
			}
		default:
			log.Warnf("'--template' is ignored because '--format %s' is specified. Use '--template' option with '--format template' option.", format)
		}
	} else if format == types.FormatTemplate {
		log.Warn("'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.")
	}

	// "--list-all-pkgs" option is unavailable with other than "--format json".
	// If user explicitly specifies "--list-all-pkgs" with "--format table" or other formats, we should warn it.
	// We check if the flag was explicitly set by the user to avoid warning when using the default value.
	if f.ListAllPkgs.IsSet() && listAllPkgs && format != types.FormatJSON {
		log.Warn(`"--list-all-pkgs" is only valid for the JSON format, for other formats a list of packages is automatically included.`)
	}

	// "--dependency-tree" option is available only with "--format table".
	if dependencyTree {
		log.Info(`"--dependency-tree" only shows the dependents of vulnerable packages. ` +
			`Note that it is the reverse of the usual dependency tree, which shows the packages that depend on the vulnerable package. ` +
			`It supports limited package managers. Please see the document for the detail.`)
		if format != types.FormatTable {
			log.Warn(`"--dependency-tree" can be used only with "--format table".`)
		}
	}

	// "--table-mode" option is available only with "--format table".
	if viper.IsSet(TableModeFlag.ConfigName) && format != types.FormatTable {
		return xerrors.New(`"--table-mode" can be used only with "--format table".`)
	}

	cs, err := loadComplianceTypes(f.Compliance.Value())
	if err != nil {
		return xerrors.Errorf("unable to load compliance spec: %w", err)
	}

	var outputPluginArgs []string
	if arg := f.OutputPluginArg.Value(); arg != "" {
		outputPluginArgs, err = shellwords.Parse(arg)
		if err != nil {
			return xerrors.Errorf("unable to parse output plugin argument: %w", err)
		}
	}

	if viper.IsSet(f.IgnoreFile.ConfigName) && !fsutils.FileExists(f.IgnoreFile.Value()) {
		return xerrors.Errorf("ignore file not found: %s", f.IgnoreFile.Value())
	}

	opts.ReportOptions = ReportOptions{
		Format:           format,
		ReportFormat:     f.ReportFormat.Value(),
		Template:         template,
		DependencyTree:   dependencyTree,
		ListAllPkgs:      listAllPkgs,
		IgnoreFile:       f.IgnoreFile.Value(),
		ExitCode:         f.ExitCode.Value(),
		ExitOnEOL:        f.ExitOnEOL.Value(),
		IgnorePolicy:     f.IgnorePolicy.Value(),
		Output:           f.Output.Value(),
		OutputPluginArgs: outputPluginArgs,
		Severities:       toSeverity(f.Severity.Value()),
		Compliance:       cs,
		ShowSuppressed:   f.ShowSuppressed.Value(),
		TableModes:       xstrings.ToTSlice[types.TableMode](tableModes),
	}
	return nil
}

func loadComplianceTypes(compliance string) (spec.ComplianceSpec, error) {
	if compliance != "" && !slices.Contains(types.SupportedCompliances, compliance) && !strings.HasPrefix(compliance, "@") {
		return spec.ComplianceSpec{}, xerrors.Errorf("unknown compliance : %v", compliance)
	}

	cs, err := spec.GetComplianceSpec(compliance, cache.DefaultDir())
	if err != nil {
		return spec.ComplianceSpec{}, xerrors.Errorf("spec loading from file system error: %w", err)
	}

	return cs, nil
}

func toSeverity(severity []string) []dbTypes.Severity {
	if len(severity) == 0 {
		return nil
	}
	severities := xslices.Map(severity, func(s string) dbTypes.Severity {
		// Note that there is no need to check the error here
		// since the severity value is already validated in the flag parser.
		sev, _ := dbTypes.NewSeverity(s)
		return sev
	})
	log.Debug("Parsed severities", log.Any("severities", severities))
	return severities
}
