package cyclonedx

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/google/uuid"

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

// Source: https://cyclonedx.org/ext/vulnerability/

// Vulnerability is the actual description of a vulnerable artifact
type Vulnerability struct {
	Ref     string   `xml:"ref,attr"`
	ID      string   `xml:"v:id"`
	Source  Source   `xml:"v:source"`
	Ratings []Rating `xml:"v:ratings>v:rating"`
	// We do not capture Common Weakness Enumeration
	// Cwes            Cwes             `xml:"v:cwes"`
	Description string `xml:"v:description,omitempty"`
	// We don't have recommendations (e.g. "upgrade")
	// Recommendations *Recommendations `xml:"v:recommendations"`
	Advisories *Advisories `xml:"v:advisories,omitempty"`
}

// Source is the origin of the vulnerability, like Github Advisories or NVD, along
// with a URL constructed with the vulnerability ID
type Source struct {
	Name string `xml:"name,attr"`
	URL  string `xml:"v:url"`
}

// Rating has information about the intensity of a vulnerability
type Rating struct {
	Score    *Score `xml:"v:score,omitempty"`
	Severity string `xml:"v:severity,omitempty"`
	Method   string `xml:"v:method,omitempty"`
	Vector   string `xml:"v:vector,omitempty"`
}

// Score provides the different ways to measure how serious a vulnerability is
type Score struct {
	Base           float64 `xml:"v:base"`
	Impact         float64 `xml:"v:impact"`
	Exploitability float64 `xml:"v:exploitability"`
}

// Advisories holds all the links for a vulnerability
type Advisories struct {
	Advisory []string `xml:"v:advisory"`
}

// 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) (string, error) {
	value, err := strconv.ParseFloat(version, 64)
	if err != nil {
		return "", err
	}

	switch int(value) {
	case 2:
		return "CVSSv2", nil
	case 3:
		return "CVSSv3", 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) (Vulnerability, error) {
	metadata, err := p.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
	if err != nil {
		return 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'
	severity := metadata.Severity
	if metadata.Severity == "Negligible" {
		severity = "Low"
	}

	var ratings = []Rating{
		{
			Severity: severity,
		},
	}
	for _, cvss := range metadata.Cvss {
		var rating Rating
		score := &Score{
			Base: cvss.Metrics.BaseScore,
		}
		if cvss.Metrics.ExploitabilityScore != nil {
			score.Exploitability = *cvss.Metrics.ExploitabilityScore
		}
		if cvss.Metrics.ImpactScore != nil {
			score.Impact = *cvss.Metrics.ImpactScore
		}
		rating.Score = score

		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.Method = method
		rating.Vector = cvss.Vector

		ratings = append(ratings, rating)
	}

	v := Vulnerability{
		Ref: uuid.New().URN(),
		ID:  m.Vulnerability.ID,
		Source: Source{
			Name: m.Vulnerability.Namespace,
			URL:  makeVulnerabilityURL(m.Vulnerability.ID),
		},
		Ratings:     ratings,
		Description: metadata.Description,
		Advisories: &Advisories{
			Advisory: metadata.URLs,
		},
	}

	return v, nil
}

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
}
