package v5

import (
	"fmt"
	"sort"
	"strings"

	qualifierV5 "github.com/anchore/grype/grype/db/v5/pkg/qualifier"
	"github.com/anchore/grype/grype/pkg/qualifier"
	"github.com/anchore/grype/grype/version"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/grype/internal/log"
	"github.com/anchore/syft/syft/cpe"
)

// 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                   `json:"id"`                      // The identifier of the vulnerability or advisory
	PackageName            string                   `json:"package_name"`            // The name of the package that is vulnerable
	Namespace              string                   `json:"namespace"`               // The ecosystem where the package resides
	PackageQualifiers      []qualifierV5.Qualifier  `json:"package_qualifiers"`      // The qualifiers for determining if a package is vulnerable
	VersionConstraint      string                   `json:"version_constraint"`      // The version range which the given package is vulnerable
	VersionFormat          string                   `json:"version_format"`          // The format which all version fields should be interpreted as
	CPEs                   []string                 `json:"cpes"`                    // The CPEs which are considered vulnerable
	RelatedVulnerabilities []VulnerabilityReference `json:"related_vulnerabilities"` // Other Vulnerabilities that are related to this one (e.g. GHSA relate to CVEs, or how distro CVE relates to NVD record)
	Fix                    Fix                      `json:"fix"`                     // All information about fixed versions
	Advisories             []Advisory               `json:"advisories"`              // Any vendor advisories about fixes or other notifications about this vulnerability
}

type VulnerabilityReference struct {
	ID        string `json:"id"`
	Namespace string `json:"namespace"`
}

//nolint:gocognit
func (v *Vulnerability) Equal(vv Vulnerability) bool {
	equal := v.ID == vv.ID &&
		v.PackageName == vv.PackageName &&
		v.Namespace == vv.Namespace &&
		len(v.PackageQualifiers) == len(vv.PackageQualifiers) &&
		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
		}
	}
	sortedBasePkgQualifiers, sortedTargetPkgQualifiers := sortPackageQualifiers(v.PackageQualifiers), sortPackageQualifiers(vv.PackageQualifiers)
	for idx, item := range sortedBasePkgQualifiers {
		if item != sortedTargetPkgQualifiers[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
}

func sortPackageQualifiers(qualifiers []qualifierV5.Qualifier) []qualifierV5.Qualifier {
	sort.SliceStable(qualifiers, func(i, j int) bool {
		return qualifiers[i].String() < qualifiers[j].String()
	})
	return qualifiers
}

func NewVulnerability(vuln Vulnerability) (*vulnerability.Vulnerability, error) {
	format := version.ParseFormat(vuln.VersionFormat)

	constraint, err := version.GetConstraint(vuln.VersionConstraint, format)
	if err != nil {
		return nil, fmt.Errorf("failed to parse constraint='%s' format='%s': %w", vuln.VersionConstraint, format, err)
	}

	pkgQualifiers := make([]qualifier.Qualifier, len(vuln.PackageQualifiers))
	for idx, q := range vuln.PackageQualifiers {
		pkgQualifiers[idx] = q.Parse()
	}

	advisories := make([]vulnerability.Advisory, len(vuln.Advisories))
	for idx, advisory := range vuln.Advisories {
		advisories[idx] = vulnerability.Advisory{
			ID:   advisory.ID,
			Link: advisory.Link,
		}
	}

	var relatedVulnerabilities []vulnerability.Reference
	for _, r := range vuln.RelatedVulnerabilities {
		relatedVulnerabilities = append(relatedVulnerabilities, vulnerability.Reference{
			ID:        r.ID,
			Namespace: r.Namespace,
		})
	}

	var cpes []cpe.CPE
	for _, cp := range vuln.CPEs {
		c, err := cpe.New(cp, "")
		if err != nil {
			log.WithFields("err", err, "cpe", cp).Debug("failed to parse CPE")
			continue
		}
		cpes = append(cpes, c)
	}

	return &vulnerability.Vulnerability{
		PackageName: vuln.PackageName,
		Constraint:  constraint,
		Reference: vulnerability.Reference{
			ID:        vuln.ID,
			Namespace: vuln.Namespace,
		},
		CPEs:              cpes,
		PackageQualifiers: pkgQualifiers,
		Fix: vulnerability.Fix{
			Versions: vuln.Fix.Versions,
			State:    vulnerability.FixState(vuln.Fix.State),
		},
		Advisories:             advisories,
		RelatedVulnerabilities: relatedVulnerabilities,
	}, nil
}
