package cyclonedx

import (
	"strconv"
	"strings"

	"github.com/CycloneDX/cyclonedx-go"
	"github.com/google/uuid"

	"github.com/anchore/grype/grype/presenter/models"
	"github.com/anchore/packageurl-go"
)

// https://cyclonedx.org/docs/1.4/json/#vulnerabilities_items_bom-ref

// NewVulnerability creates a Vulnerability document from a match and the metadata provider
func NewVulnerability(m models.Match) (v cyclonedx.Vulnerability, err error) {
	metadata := m.Vulnerability.VulnerabilityMetadata

	ratings := generateCDXRatings(metadata)

	source := &cyclonedx.Source{
		Name: cdxSourceName(metadata.Namespace),
		URL:  metadata.DataSource,
	}

	references := &[]cyclonedx.VulnerabilityReference{
		{
			ID:     m.Vulnerability.ID,
			Source: source,
		},
	}

	advisories := &[]cyclonedx.Advisory{}
	for _, advisory := range metadata.URLs {
		*advisories = append(*advisories, cyclonedx.Advisory{
			URL: advisory,
		})
	}

	// Note: if a field isn't captured here it's usually because the resulting
	// reference link contains that information for the consumer
	return cyclonedx.Vulnerability{
		BOMRef:     uuid.New().URN(),
		ID:         m.Vulnerability.ID,
		Source:     source,
		References: references,
		Ratings:    &ratings,
		// We do not capture CWEs in our model
		CWEs:        nil,
		Description: metadata.Description,
		// We do not capture the full detailed description in our model
		Detail: "",
		// We do not capture the recommendations in our model
		Recommendation: "",
		Advisories:     advisories,
		Affects: &[]cyclonedx.Affects{
			{
				Ref: deriveBomRef(m.Artifact),
			},
		},
		// Data source creation
		Created: "",
		// Vulnerability first published
		Published: "",
		// Vulnerability last updated
		Updated: "",
		// We do not capture acredited in our model
		Credits: nil,
		// We do not capture information about the  method used to determine the vulnerability pre publishing
		Tools: nil,
		// TODO:  we do not leverage the following fields in our model
		Analysis:   nil,
		Properties: nil,
	}, nil
}

func generateCDXRatings(metadata models.VulnerabilityMetadata) []cyclonedx.VulnerabilityRating {
	severity := cdxSeverityFromGrypeSeverity(metadata.Severity)

	ratings := make([]cyclonedx.VulnerabilityRating, 0)
	for _, cvss := range metadata.Cvss {
		var rating cyclonedx.VulnerabilityRating
		score := cvss.Metrics.BaseScore
		rating.Score = &score

		// Scoring method can be one of the following:
		// "CVSSv2", "CVSSv3", "CVSSv31", "OWASP", "other"
		method, err := cvssVersionToMethod(cvss.Version)
		if err != nil {
			// do not halt execution if one CVSS fails to provide an accurate Version
			// TODO: log warning here?
			continue
		}
		rating.Method = method
		rating.Vector = cvss.Vector
		rating.Severity = severity
		ratings = append(ratings, rating)
	}

	// ensure the severity is always included
	if len(ratings) == 0 {
		ratings = append(ratings, cyclonedx.VulnerabilityRating{
			Severity: severity,
		})
	}

	// Add EPSS score if available
	if len(metadata.EPSS) > 0 {
		epssScore := metadata.EPSS[0].EPSS

		ratings = append(ratings, cyclonedx.VulnerabilityRating{
			Method: cyclonedx.ScoringMethod("EPSS"),
			Score:  &epssScore,
			Source: &cyclonedx.Source{
				Name: "FIRST",
				URL:  "https://www.first.org/epss/",
			},
		})
	}
	// Add KEV indication if available
	if len(metadata.KnownExploited) > 0 {
		kevScore := 1.0

		ratings = append(ratings, cyclonedx.VulnerabilityRating{
			Method: cyclonedx.ScoringMethodOther,
			Score:  &kevScore,
			Source: &cyclonedx.Source{
				Name: "CISA KEV Catalog",
				URL:  "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
			},
			Justification: "Listed in CISA KEV",
		})
	}

	return ratings
}

// cvssVersionToMethod accepts a CVSS version as string (e.g. "3.1") and converts it to a
// CycloneDx rating Method, for example "CVSSv3"
func cvssVersionToMethod(version string) (cyclonedx.ScoringMethod, error) {
	value, err := strconv.ParseFloat(version, 64)
	if err != nil {
		return "", err
	}

	switch value {
	case 2:
		return cyclonedx.ScoringMethodCVSSv2, nil
	case 3:
		return cyclonedx.ScoringMethodCVSSv3, nil
	case 3.1:
		return cyclonedx.ScoringMethodCVSSv31, nil
	default:
		return cyclonedx.ScoringMethodOther, nil
	}
}

// takes namespace: eg debian:distro:debian:10
// returns source name: eg debian-distrot-debian-10
func cdxSourceName(namespace string) string {
	return strings.ReplaceAll(namespace, ":", "-")
}

func cdxSeverityFromGrypeSeverity(severity string) cyclonedx.Severity {
	switch severity {
	case "Negligible":
		return cyclonedx.SeverityNone
	case "Unknown":
		return cyclonedx.SeverityUnknown
	case "Info":
		return cyclonedx.SeverityInfo
	case "Low":
		return cyclonedx.SeverityLow
	case "Medium":
		return cyclonedx.SeverityMedium
	case "High":
		return cyclonedx.SeverityHigh
	case "Critical":
		return cyclonedx.SeverityCritical
	default:
		return cyclonedx.SeverityUnknown
	}
}

func deriveBomRef(p models.Package) string {
	// try and parse the PURL if possible and append syft id to it, to make
	// the purl unique in the BOM.
	// TODO: In the future we may want to dedupe by PURL and combine components with
	// the same PURL while preserving their unique metadata.
	if parsedPURL, err := packageurl.FromString(p.PURL); err == nil {
		parsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: "package-id", Value: p.ID})
		return parsedPURL.ToString()
	}
	// fallback is to use strictly the ID if there is no valid pURL
	return p.ID
}
