package v3

import (
	"sort"
	"strings"
)

// Vulnerability represents the minimum data fields necessary to perform package-to-vulnerability matching. This can represent a CVE, 3rd party advisory, or any source that relates back to a CVE.
type Vulnerability struct {
	ID                     string                   // The identifier of the vulnerability or advisory
	PackageName            string                   // The name of the package that is vulnerable
	Namespace              string                   // The ecosystem where the package resides
	VersionConstraint      string                   // The version range which the given package is vulnerable
	VersionFormat          string                   // The format which all version fields should be interpreted as
	CPEs                   []string                 // The CPEs which are considered vulnerable
	RelatedVulnerabilities []VulnerabilityReference // Other Vulnerabilities that are related to this one (e.g. GHSA relate to CVEs, or how distro CVE relates to NVD record)
	Fix                    Fix                      // All information about fixed versions
	Advisories             []Advisory               // Any vendor advisories about fixes or other notifications about this vulnerability
}

type VulnerabilityReference struct {
	ID        string
	Namespace string
}

//nolint:gocognit
func (v *Vulnerability) Equal(vv Vulnerability) bool {
	equal := v.ID == vv.ID &&
		v.PackageName == vv.PackageName &&
		v.Namespace == vv.Namespace &&
		v.VersionConstraint == vv.VersionConstraint &&
		v.VersionFormat == vv.VersionFormat &&
		len(v.CPEs) == len(vv.CPEs) &&
		len(v.RelatedVulnerabilities) == len(vv.RelatedVulnerabilities) &&
		len(v.Advisories) == len(vv.Advisories) &&
		v.Fix.State == vv.Fix.State &&
		len(v.Fix.Versions) == len(vv.Fix.Versions)

	if !equal {
		return false
	}

	sort.Strings(v.CPEs)
	sort.Strings(vv.CPEs)
	for idx, cpe := range v.CPEs {
		if cpe != vv.CPEs[idx] {
			return false
		}
	}

	sortedBaseRelVulns, sortedTargetRelVulns := sortRelatedVulns(v.RelatedVulnerabilities), sortRelatedVulns(vv.RelatedVulnerabilities)
	for idx, item := range sortedBaseRelVulns {
		if item != sortedTargetRelVulns[idx] {
			return false
		}
	}
	sortedBaseAdvisories, sortedTargetAdvisories := sortAdvisories(v.Advisories), sortAdvisories(vv.Advisories)
	for idx, item := range sortedBaseAdvisories {
		if item != sortedTargetAdvisories[idx] {
			return false
		}
	}
	sort.Strings(v.Fix.Versions)
	sort.Strings(vv.Fix.Versions)
	for idx, item := range v.Fix.Versions {
		if item != vv.Fix.Versions[idx] {
			return false
		}
	}

	return true
}

func sortRelatedVulns(vulns []VulnerabilityReference) []VulnerabilityReference {
	sort.SliceStable(vulns, func(i, j int) bool {
		b1, b2 := strings.Builder{}, strings.Builder{}
		b1.WriteString(vulns[i].ID)
		b1.WriteString(vulns[i].Namespace)
		b2.WriteString(vulns[j].ID)
		b2.WriteString(vulns[j].Namespace)
		return b1.String() < b2.String()
	})
	return vulns
}

func sortAdvisories(advisories []Advisory) []Advisory {
	sort.SliceStable(advisories, func(i, j int) bool {
		b1, b2 := strings.Builder{}, strings.Builder{}
		b1.WriteString(advisories[i].ID)
		b1.WriteString(advisories[i].Link)
		b2.WriteString(advisories[j].ID)
		b2.WriteString(advisories[j].Link)
		return b1.String() < b2.String()
	})
	return advisories
}
