package vulnerability

import (
	"bufio"
	"os"
	"sort"
	"strings"

	"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"

	"github.com/google/wire"

	"github.com/aquasecurity/trivy-db/pkg/db"

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

const (
	DefaultIgnoreFile = ".trivyignore"
)

var SuperSet = wire.NewSet(
	wire.Struct(new(db.Config)),
	NewClient,
	wire.Bind(new(Operation), new(Client)),
)

type Operation interface {
	FillInfo(vulns []types.DetectedVulnerability, reportType string)
	Filter(vulns []types.DetectedVulnerability, severities []dbTypes.Severity,
		ignoreUnfixed bool, ignoreFile string) []types.DetectedVulnerability
}

type Client struct {
	dbc db.Operation
}

func NewClient(dbc db.Config) Client {
	return Client{dbc: dbc}
}

func (c Client) FillInfo(vulns []types.DetectedVulnerability, reportType string) {
	var err error

	for i := range vulns {
		vulns[i].Vulnerability, err = c.dbc.GetVulnerability(vulns[i].VulnerabilityID)
		if err != nil {
			log.Logger.Warnf("Error while getting vulnerability details: %s\n", err)
			continue
		}

		var source string
		switch reportType {
		case vulnerability.Ubuntu, vulnerability.Alpine, vulnerability.RedHat, vulnerability.RedHatOVAL, vulnerability.Debian, vulnerability.DebianOVAL, vulnerability.Fedora, vulnerability.Amazon, vulnerability.OracleOVAL, vulnerability.SuseCVRF, vulnerability.OpenSuseCVRF, vulnerability.Photon:
			source = reportType
		case vulnerability.CentOS: // CentOS doesn't have its own so we use RedHat
			source = vulnerability.RedHat
		case "npm", "yarn":
			source = vulnerability.NodejsSecurityWg
		case "pipenv", "poetry":
			source = vulnerability.PythonSafetyDB
		case "bundler":
			source = vulnerability.RubySec
		case "cargo":
			source = vulnerability.RustSec
		case "composer":
			source = vulnerability.PhpSecurityAdvisories
		}
		c.getVendorSeverity(&vulns[i], source)
		vulns[i].Vulnerability.VendorSeverity = nil // Remove VendorSeverity from Results
	}
}

func (c Client) getVendorSeverity(vuln *types.DetectedVulnerability, reportType string) {
	if vs, ok := vuln.Vulnerability.VendorSeverity[reportType]; ok {
		vuln.Vulnerability.Severity = vs.String()
		vuln.SeveritySource = reportType
		return
	}

	// Try NVD as a fallback if it exists
	if vs, ok := vuln.Vulnerability.VendorSeverity[vulnerability.Nvd]; ok {
		vuln.Vulnerability.Severity = vs.String()
		vuln.SeveritySource = vulnerability.Nvd
		return
	}
}

func (c Client) Filter(vulns []types.DetectedVulnerability, severities []dbTypes.Severity,
	ignoreUnfixed bool, ignoreFile string) []types.DetectedVulnerability {
	ignoredIDs := getIgnoredIDs(ignoreFile)
	var vulnerabilities []types.DetectedVulnerability
	for _, vuln := range vulns {
		// Filter vulnerabilities by severity
		for _, s := range severities {
			if s.String() == vuln.Severity {
				// Ignore unfixed vulnerabilities
				if ignoreUnfixed && vuln.FixedVersion == "" {
					continue
				} else if utils.StringInSlice(vuln.VulnerabilityID, ignoredIDs) {
					continue
				}
				vulnerabilities = append(vulnerabilities, vuln)
				break
			}
		}
	}
	sort.Slice(vulnerabilities, func(i, j int) bool {
		if vulnerabilities[i].PkgName != vulnerabilities[j].PkgName {
			return vulnerabilities[i].PkgName < vulnerabilities[j].PkgName
		}
		ret := dbTypes.CompareSeverityString(
			vulnerabilities[j].Severity, vulnerabilities[i].Severity,
		)
		if ret != 0 {
			return ret > 0
		}
		return vulnerabilities[i].VulnerabilityID < vulnerabilities[j].VulnerabilityID
	})
	return vulnerabilities
}

func getIgnoredIDs(ignoreFile string) []string {
	f, err := os.Open(ignoreFile)
	if err != nil {
		// trivy must work even if no .trivyignore exist
		return nil
	}

	var ignoredIDs []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "#") || line == "" {
			continue
		}
		ignoredIDs = append(ignoredIDs, line)
	}
	return ignoredIDs
}
