package cyclonedx

import (
	"fmt"
	"strconv"
	"strings"

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

	"github.com/anchore/grype/grype/match"
	"github.com/anchore/grype/grype/pkg"
	"github.com/anchore/grype/grype/vulnerability"
	"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 match.Match, p vulnerability.MetadataProvider) (v cyclonedx.Vulnerability, err error) {
	metadata, err := p.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
	if err != nil {
		return v, fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
	}

	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.Package),
			},
		},
		// 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 *vulnerability.Metadata) []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,
		})
	}

	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 pkg.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: string(p.ID)})
		return parsedPURL.ToString()
	}
	// fallback is to use strictly the ID if there is no valid pURL
	return string(p.ID)
}
