package vulnerability

import (
	"strings"
	"sync"

	"github.com/google/wire"
	"github.com/samber/lo"

	"github.com/aquasecurity/trivy-db/pkg/db"
	dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
	"github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability"
	"github.com/aquasecurity/trivy/pkg/log"
	"github.com/aquasecurity/trivy/pkg/types"
	"github.com/aquasecurity/trivy/pkg/version/doc"
)

var (
	primaryURLPrefixes = map[dbTypes.SourceID][]string{
		vulnerability.Debian: {
			"http://www.debian.org",
			"https://www.debian.org",
		},
		vulnerability.Ubuntu: {
			"http://www.ubuntu.com",
			"https://usn.ubuntu.com",
		},
		vulnerability.RedHat: {"https://access.redhat.com"},
		vulnerability.SuseCVRF: {
			"http://lists.opensuse.org",
			"https://lists.opensuse.org",
		},
		vulnerability.OracleOVAL: {
			"http://linux.oracle.com/errata",
			"https://linux.oracle.com/errata",
		},
		vulnerability.NodejsSecurityWg: {
			"https://www.npmjs.com",
			"https://hackerone.com",
		},
		vulnerability.RubySec: {"https://groups.google.com"},
	}
)

// SuperSet binds the dependencies
var SuperSet = wire.NewSet(
	wire.Struct(new(db.Config)),
	wire.Bind(new(db.Operation), new(db.Config)),
	NewClient,
)

// Show warning if we use severity from another vendor
// cf. https://github.com/aquasecurity/trivy/issues/6714
var onceWarn = sync.OnceFunc(func() {
	// e.g. https://aquasecurity.github.io/trivy/latest/docs/scanner/vulnerability/#severity-selection
	log.Warnf("Using severities from other vendors for some vulnerabilities. Read %s for details.", doc.URL("/docs/scanner/vulnerability/", "severity-selection"))
})

// Client manipulates vulnerabilities
type Client struct {
	dbc db.Operation
}

// NewClient is the factory method for Client
func NewClient(dbc db.Operation) Client {
	return Client{dbc: dbc}
}

// FillInfo fills extra info in vulnerability objects
func (c Client) FillInfo(vulns []types.DetectedVulnerability) {
	for i := range vulns {
		// Add the vulnerability status
		// Some vendors such as Red Hat have their own vulnerability status, and we use it.
		// Otherwise, we put "fixed" or "affected" according to the fixed version.
		if vulns[i].FixedVersion != "" {
			vulns[i].Status = dbTypes.StatusFixed
		} else if vulns[i].Status == dbTypes.StatusUnknown {
			vulns[i].Status = dbTypes.StatusAffected
		}

		// Get the vulnerability detail
		vulnID := vulns[i].VulnerabilityID
		vuln, err := c.dbc.GetVulnerability(vulnID)
		if err != nil {
			log.Warn("Unable to get vulnerability details (CVE may be rejected)", log.Err(err))
			continue
		}

		// Detect the data source
		dataSource := lo.FromPtr(vulns[i].DataSource)

		// Select the severity according to the detected sourceID.
		severity, severitySource := c.getVendorSeverity(vulnID, &vuln, dataSource)

		// The vendor might provide package-specific severity like Debian.
		// For example, CVE-2015-2328 in Debian has "unimportant" for mongodb and "low" for pcre3.
		// In that case, we keep the severity as is.
		if vulns[i].SeveritySource != "" {
			severity = vulns[i].Severity
			severitySource = vulns[i].SeveritySource

			// Store package-specific severity in vendor severities
			if vuln.VendorSeverity == nil {
				vuln.VendorSeverity = make(dbTypes.VendorSeverity)
			}
			s, _ := dbTypes.NewSeverity(severity) // skip error handling because `SeverityUnknown` will be returned in case of error
			vuln.VendorSeverity[severitySource] = s
		}

		// Add the vulnerability detail
		vulns[i].Vulnerability = vuln

		vulns[i].Severity = severity
		vulns[i].SeveritySource = severitySource
		vulns[i].PrimaryURL = c.getPrimaryURL(vulnID, vuln.References, dataSource.ID)
	}
}

func (c Client) getVendorSeverity(vulnID string, vuln *dbTypes.Vulnerability, dataSource dbTypes.DataSource) (string, dbTypes.SourceID) {
	if vs, ok := vuln.VendorSeverity[dataSource.ID]; ok {
		return vs.String(), dataSource.ID
	}

	// use severity from GitHub for all GHSA-xxx vulnerabilities
	if strings.HasPrefix(vulnID, "GHSA-") {
		if vs, ok := vuln.VendorSeverity[vulnerability.GHSA]; ok {
			return vs.String(), vulnerability.GHSA
		}
	}

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

	if vuln.Severity == "" {
		return dbTypes.SeverityUnknown.String(), ""
	}

	onceWarn()
	return vuln.Severity, ""
}

func (c Client) getPrimaryURL(vulnID string, refs []string, source dbTypes.SourceID) string {
	switch {
	case strings.HasPrefix(vulnID, "CVE-"):
		return "https://avd.aquasec.com/nvd/" + strings.ToLower(vulnID)
	case strings.HasPrefix(vulnID, "RUSTSEC-"):
		return "https://osv.dev/vulnerability/" + vulnID
	case strings.HasPrefix(vulnID, "GHSA-"):
		return "https://github.com/advisories/" + vulnID
	case strings.HasPrefix(vulnID, "TEMP-"):
		return "https://security-tracker.debian.org/tracker/" + vulnID
	}

	prefixes := primaryURLPrefixes[source]
	for _, pre := range prefixes {
		for _, ref := range refs {
			if strings.HasPrefix(ref, pre) {
				return ref
			}
		}
	}
	return ""
}
