package v6

import (
	"fmt"
	"sort"
	"strings"

	"github.com/scylladb/go-set/strset"

	"github.com/anchore/grype/grype/pkg/qualifier"
	"github.com/anchore/grype/grype/pkg/qualifier/platformcpe"
	"github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity"
	"github.com/anchore/grype/grype/version"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/grype/internal/log"
	"github.com/anchore/syft/syft/cpe"
	"github.com/anchore/syft/syft/pkg"
)

const (
	nvdProvider    = "nvd"
	githubProvider = "github"
	v5NvdNamespace = "nvd:cpe"
)

func newVulnerabilityFromAffectedPackageHandle(affected AffectedPackageHandle, affectedRanges []Range) (*vulnerability.Vulnerability, error) {
	packageName := ""
	if affected.Package != nil {
		packageName = affected.Package.Name
	}

	if affected.Vulnerability == nil || affected.Vulnerability.BlobValue == nil || affected.BlobValue == nil {
		return nil, fmt.Errorf("nil data when attempting to create vulnerability from AffectedPackageHandle")
	}

	return newVulnerabilityFromParts(packageName, affected.Vulnerability, affected.BlobValue, affectedRanges, &affected, nil)
}

func newVulnerabilityFromAffectedCPEHandle(affected AffectedCPEHandle, affectedRanges []Range) (*vulnerability.Vulnerability, error) {
	if affected.Vulnerability == nil || affected.Vulnerability.BlobValue == nil || affected.BlobValue == nil {
		return nil, fmt.Errorf("nil data when attempting to create vulnerability from AffectedCPEHandle")
	}
	return newVulnerabilityFromParts(affected.CPE.Product, affected.Vulnerability, affected.BlobValue, affectedRanges, nil, &affected)
}

func newVulnerabilityFromUnaffectedPackageHandle(unaffected UnaffectedPackageHandle, unaffectedRanges []Range) (*vulnerability.Vulnerability, error) {
	packageName := ""
	if unaffected.Package != nil {
		packageName = unaffected.Package.Name
	}

	if unaffected.Vulnerability == nil || unaffected.Vulnerability.BlobValue == nil || unaffected.BlobValue == nil {
		return nil, fmt.Errorf("nil data when attempting to create vulnerability from UnaffectedPackageHandle")
	}

	vuln, err := newVulnerabilityFromParts(packageName, unaffected.Vulnerability, unaffected.BlobValue, unaffectedRanges, (*AffectedPackageHandle)(&unaffected), nil)
	if vuln != nil {
		vuln.Unaffected = true
	}
	return vuln, err
}

func newVulnerabilityFromUnaffectedCPEHandle(unaffected UnaffectedCPEHandle, unaffectedRanges []Range) (*vulnerability.Vulnerability, error) {
	if unaffected.Vulnerability == nil || unaffected.Vulnerability.BlobValue == nil || unaffected.BlobValue == nil {
		return nil, fmt.Errorf("nil data when attempting to create vulnerability from UnaffectedCPEHandle")
	}
	vuln, err := newVulnerabilityFromParts(unaffected.CPE.Product, unaffected.Vulnerability, unaffected.BlobValue, unaffectedRanges, nil, (*AffectedCPEHandle)(&unaffected))
	if vuln != nil {
		vuln.Unaffected = true
	}
	return vuln, err
}

func newVulnerabilityFromParts(packageName string, vuln *VulnerabilityHandle, pkgBlob *PackageBlob, ranges []Range, affectedPackageHandle *AffectedPackageHandle, affectedCpeHandle *AffectedCPEHandle) (*vulnerability.Vulnerability, error) {
	if vuln.BlobValue == nil {
		return nil, fmt.Errorf("vuln has no blob value: %+v", vuln)
	}

	constraint, err := getVersionConstraint(ranges)
	if err != nil {
		return nil, nil
	}

	var language string
	if affectedPackageHandle != nil && affectedPackageHandle.Package != nil {
		language = affectedPackageHandle.Package.Ecosystem
	}

	v5namespace := MimicV5Namespace(vuln, affectedPackageHandle)
	return &vulnerability.Vulnerability{
		Reference: vulnerability.Reference{
			ID:        vuln.Name,
			Namespace: v5namespace,
			Internal:  vuln, // just hold a reference to the vulnHandle for later use
		},
		PackageName:            packageName,
		PackageQualifiers:      getPackageQualifiers(pkgBlob),
		Constraint:             constraint,
		CPEs:                   toCPEs(affectedPackageHandle, affectedCpeHandle),
		RelatedVulnerabilities: getRelatedVulnerabilities(vuln, pkgBlob, language),
		Fix:                    toFix(ranges),
		Advisories:             toAdvisories(ranges),
		Status:                 string(vuln.Status),
	}, nil
}

func getVersionConstraint(affectedRanges []Range) (version.Constraint, error) {
	var constraints []string
	types := strset.New()
	for _, r := range affectedRanges {
		if r.Version.Constraint != "" {
			if r.Version.Type != "" {
				types.Add(r.Version.Type)
			}

			constraints = append(constraints, r.Version.Constraint)
		}
	}

	if types.Size() > 1 {
		log.WithFields("types", types.List()).Debug("multiple version formats found for a single vulnerability")
	}

	var ty string
	if types.Size() >= 1 {
		typeStrs := types.List()
		sort.Strings(typeStrs)
		ty = typeStrs[0]
	}

	versionFormat := version.ParseFormat(ty)
	constraint, err := version.GetConstraint(strings.Join(constraints, ","), versionFormat)
	if err != nil {
		log.WithFields("error", err, "constraint", constraints).Debug("unable to parse constraint")
		return nil, err
	}
	return constraint, nil
}

// getRelatedVulnerabilities returns a list of related vulnerabilities based on the vulnerability ID, aliases, and CVEs from the affected package (if available).
func getRelatedVulnerabilities(vuln *VulnerabilityHandle, affected *PackageBlob, language string) []vulnerability.Reference {
	var relatedVulnerabilities []vulnerability.Reference
	idsToProcess := append([]string{vuln.Name}, vuln.BlobValue.Aliases...)

	if affected != nil {
		idsToProcess = append(idsToProcess, affected.CVEs...)
	}

	encountered := strset.New()
	for _, id := range idsToProcess {
		if encountered.Has(id) {
			continue
		}

		lowerID := strings.ToLower(id)

		switch {
		case strings.HasPrefix(lowerID, "cve-"):
			if vuln.ProviderID == nvdProvider && strings.EqualFold(vuln.Name, id) {
				// the original vuln is an NVD CVE, so we don't need to add a self-reference
				continue
			}

			relatedVulnerabilities = append(relatedVulnerabilities,
				vulnerability.Reference{
					ID:        id,
					Namespace: v5NvdNamespace,
				},
			)
		case strings.HasPrefix(lowerID, "ghsa-"):
			if vuln.ProviderID == githubProvider && strings.EqualFold(vuln.Name, id) {
				// the original vuln is a GitHub GHSA, so we don't need to add a self-reference
				continue
			}

			relatedVulnerabilities = append(relatedVulnerabilities,
				vulnerability.Reference{
					ID:        id,
					Namespace: mimicV5GithubNamespace(githubProvider, language),
				},
			)
		}

		encountered.Add(id)
	}

	return relatedVulnerabilities
}

func getPackageQualifiers(affected *PackageBlob) []qualifier.Qualifier {
	if affected != nil {
		return toPackageQualifiers(affected.Qualifiers)
	}

	return nil
}

// MimicV5Namespace returns the namespace for a given affected package based on what schema v5 did.
//
//nolint:funlen
func MimicV5Namespace(vuln *VulnerabilityHandle, affected *AffectedPackageHandle) string {
	if affected == nil || affected.Package == nil { // for CPE matches
		return fmt.Sprintf("%s:cpe", vuln.Provider.ID)
	}

	if affected.OperatingSystem != nil {
		// distro family fixes
		family := affected.OperatingSystem.Name
		ver := affected.OperatingSystem.Version()
		switch affected.OperatingSystem.Name {
		case "amazon":
			family = "amazonlinux"
		case "mariner", "azurelinux":
			fields := strings.Split(ver, ".")
			major := fields[0]
			switch len(fields) {
			case 1:
				ver = fmt.Sprintf("%s.0", major)
			default:
				ver = fmt.Sprintf("%s.%s", major, fields[1])
			}
			switch major {
			case "1", "2":
				family = "mariner"
			default:
				family = "azurelinux"
			}
		case "ubuntu":
			if strings.Count(ver, ".") == 1 {
				// convert 20.4 to 20.04
				fields := strings.Split(ver, ".")
				major, minor := fields[0], fields[1]
				if len(minor) == 1 {
					ver = fmt.Sprintf("%s.0%s", major, minor)
				}
			}
		case "oracle":
			family = "oraclelinux"
		}

		// provider fixes
		pr := vuln.Provider.ID
		if pr == "rhel" {
			pr = "redhat"
		}

		// version fixes
		switch vuln.Provider.ID {
		case "rhel", "oracle":
			// ensure we only keep the major version
			ver = strings.Split(ver, ".")[0]
		}

		return fmt.Sprintf("%s:distro:%s:%s", pr, family, ver)
	}

	if affected.Package != nil {
		language := affected.Package.Ecosystem
		switch strings.ToLower(language) {
		case "msrc", string(pkg.KbPkg): // msrc packages were previously modelled as distro
			return fmt.Sprintf("%s:distro:windows:%s", vuln.Provider.ID, affected.Package.Name)
		case string(pkg.BitnamiPkg): // bitnami packages were previously modelled as distro
			return "bitnami"
		case "": // CPE
			return fmt.Sprintf("%s:cpe", vuln.Provider.ID)
		}
		return mimicV5GithubNamespace(vuln.Provider.ID, affected.Package.Ecosystem)
	}

	// this shouldn't happen and is not a valid v5 namespace, but some information is better than none
	return vuln.Provider.ID
}

func mimicV5GithubNamespace(provider, language string) string {
	// normalize from purl type, github ecosystem types, and vunnel mappings
	switch strings.ToLower(language) {
	case "golang", string(pkg.GoModulePkg):
		language = "go"
	case "composer", string(pkg.PhpComposerPkg):
		language = "php"
	case "cargo", string(pkg.RustPkg):
		language = "rust"
	case "pub", string(pkg.DartPubPkg):
		language = "dart"
	case "nuget", string(pkg.DotnetPkg):
		language = "dotnet"
	case "maven", string(pkg.JavaPkg), string(pkg.JenkinsPluginPkg):
		language = "java"
	case "swifturl", string(pkg.SwiplPackPkg), string(pkg.SwiftPkg):
		language = "swift"
	case "node", string(pkg.NpmPkg):
		language = "javascript"
	case "pypi", "pip", string(pkg.PythonPkg):
		language = "python"
	case "rubygems", string(pkg.GemPkg):
		language = "ruby"
	}
	return fmt.Sprintf("%s:language:%s", provider, language)
}

func toPackageQualifiers(qualifiers *PackageQualifiers) []qualifier.Qualifier {
	if qualifiers == nil {
		return nil
	}
	var out []qualifier.Qualifier
	for _, c := range qualifiers.PlatformCPEs {
		out = append(out, platformcpe.New(c))
	}
	if qualifiers.RpmModularity != nil {
		out = append(out, rpmmodularity.New(*qualifiers.RpmModularity))
	}
	return out
}

func toFix(affectedRanges []Range) vulnerability.Fix {
	var state vulnerability.FixState
	var versions []string
	var availables []vulnerability.FixAvailable
	for _, r := range affectedRanges {
		if r.Fix == nil {
			continue
		}
		switch r.Fix.State {
		case FixedStatus:
			state = vulnerability.FixStateFixed
			versions = append(versions, r.Fix.Version)
			if r.Fix.Detail != nil && r.Fix.Detail.Available != nil {
				a := r.Fix.Detail.Available
				if a.Date != nil {
					availables = append(availables, vulnerability.FixAvailable{
						Version: r.Fix.Version,
						Date:    *a.Date,
						Kind:    a.Kind,
					})
				}
			}
		case NotAffectedFixStatus:
			// TODO: not handled yet
		case WontFixStatus:
			if state != vulnerability.FixStateFixed {
				state = vulnerability.FixStateWontFix
			}
		case NotFixedStatus:
			if state != vulnerability.FixStateFixed {
				state = vulnerability.FixStateNotFixed
			}
		}
	}
	if len(versions) == 0 && state == "" {
		return vulnerability.Fix{}
	}
	return vulnerability.Fix{
		Versions:  versions,
		State:     state,
		Available: availables,
	}
}

func toAdvisories(affectedRanges []Range) []vulnerability.Advisory {
	var advisories []vulnerability.Advisory
	for _, r := range affectedRanges {
		if r.Fix == nil || r.Fix.Detail == nil {
			continue
		}
		for _, urlRef := range r.Fix.Detail.References {
			if urlRef.URL == "" {
				continue
			}
			advisories = append(advisories, vulnerability.Advisory{
				ID:   urlRef.ID,
				Link: urlRef.URL,
			})
		}
	}

	return advisories
}

func toCPEs(affectedPackageHandle *AffectedPackageHandle, affectedCPEHandle *AffectedCPEHandle) []cpe.CPE {
	var out []cpe.CPE
	var cpes []Cpe
	if affectedPackageHandle != nil {
		cpes = affectedPackageHandle.Package.CPEs
	}
	if affectedCPEHandle != nil && affectedCPEHandle.CPE != nil {
		cpes = append(cpes, *affectedCPEHandle.CPE)
	}
	for _, c := range cpes {
		out = append(out, cpe.CPE{
			Attributes: cpe.Attributes{
				Part:      c.Part,
				Vendor:    c.Vendor,
				Product:   c.Product,
				Version:   cpe.Any,
				Update:    cpe.Any,
				Edition:   c.Edition,
				SWEdition: c.SoftwareEdition,
				TargetSW:  c.TargetSoftware,
				TargetHW:  c.TargetHardware,
				Other:     c.Other,
				Language:  c.Language,
			},
			Source: "",
		})
	}
	return out
}
