package cyclonedxvex

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/CycloneDX/cyclonedx-go"

	grypeDb "github.com/anchore/grype/grype/db/v5"
	"github.com/anchore/grype/grype/match"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/grype/internal/log"
)

// 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 "", fmt.Errorf("unable to parse %s into a CVSS version", version)
	}
}

// NewVulnerability creates a Vulnerability document from a match and the metadata provider
func NewVulnerability(m match.Match, p vulnerability.MetadataProvider) (cyclonedx.Vulnerability, error) {
	metadata, err := p.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
	if err != nil {
		return cyclonedx.Vulnerability{}, fmt.Errorf("unable to fetch vuln=%q metadata: %+v", m.Vulnerability.ID, err)
	}

	// The schema does not allow "Negligible", only allowing the following:
	// 'None', 'Low', 'Medium', 'High', 'Critical', 'Unknown'
	severityMap := map[string]cyclonedx.Severity{
		"unknown":    cyclonedx.SeverityUnknown,
		"none":       cyclonedx.SeverityNone,
		"info":       cyclonedx.SeverityInfo,
		"negligible": cyclonedx.SeverityLow,
		"low":        cyclonedx.SeverityLow,
		"medium":     cyclonedx.SeverityMedium,
		"high":       cyclonedx.SeverityHigh,
		"critical":   cyclonedx.SeverityCritical,
	}
	severity, ok := severityMap[strings.ToLower(metadata.Severity)]
	if !ok {
		severity = cyclonedx.SeverityUnknown
	}
	var ratings = []cyclonedx.VulnerabilityRating{
		{
			Severity: severity,
		},
	}
	for _, cvss := range metadata.Cvss {
		method, err := cvssVersionToMethod(cvss.Version)
		if err != nil {
			log.Errorf("unable to parse CVSS version: %v", err)
			// do not halt execution if one CVSS fails to provide an accurate Version
			continue
		}
		rating := cyclonedx.VulnerabilityRating{
			Method: method,
			Vector: cvss.Vector,
			Score:  &cvss.Metrics.BaseScore,
		}

		ratings = append(ratings, rating)
	}
	advisories := []cyclonedx.Advisory{}
	for _, url := range metadata.URLs {
		advisories = append(advisories, cyclonedx.Advisory{
			URL: url,
		})
	}
	v := cyclonedx.Vulnerability{
		ID: m.Vulnerability.ID,
		Source: &cyclonedx.Source{
			Name: m.Vulnerability.Namespace,
			URL:  makeVulnerabilityURL(m.Vulnerability.ID),
		},
		Ratings:     &ratings,
		Description: metadata.Description,
		Advisories:  &advisories,
		Analysis: &cyclonedx.VulnerabilityAnalysis{
			State: cyclonedx.IASInTriage,
		},
		Properties: makeProperties(m.Vulnerability.Fix),
	}

	return v, nil
}

func makeProperties(fix vulnerability.Fix) *[]cyclonedx.Property {
	properties := []cyclonedx.Property{}
	if fix.State == grypeDb.FixedState {
		properties = append(properties, cyclonedx.Property{
			Name:  "grype:fixed_versions",
			Value: strings.Join(fix.Versions, ","),
		})
	}
	return &properties
}

func makeVulnerabilityURL(id string) string {
	if strings.HasPrefix(id, "CVE-") {
		return fmt.Sprintf("http://cve.mitre.org/cgi-bin/cvename.cgi?name=%s", id)
	}
	if strings.HasPrefix(id, "GHSA") {
		return fmt.Sprintf("https://github.com/advisories/%s", id)
	}
	return id
}
