package github

import (
	"errors"
	"fmt"
	"strings"

	"github.com/anchore/grype/grype/db/data"
	"github.com/anchore/grype/grype/db/internal/provider/unmarshal"
	"github.com/anchore/grype/grype/db/internal/versionutil"
	db "github.com/anchore/grype/grype/db/v5"
	"github.com/anchore/grype/grype/db/v5/build/transformers"
	"github.com/anchore/grype/grype/db/v5/namespace"
	syftPkg "github.com/anchore/syft/syft/pkg"
)

var errSkip = fmt.Errorf("skipping advisory")

func buildGrypeNamespace(group string) (namespace.Namespace, error) {
	feedGroupComponents := strings.Split(group, ":")

	if len(feedGroupComponents) < 2 {
		return nil, fmt.Errorf("unable to determine grype namespace for enterprise namespace=%s", group)
	}

	feedGroupLang := feedGroupComponents[1]
	syftLanguage := syftPkg.LanguageByName(feedGroupLang)

	if syftLanguage == syftPkg.UnknownLanguage {
		switch feedGroupLang {
		case "nuget":
			syftLanguage = syftPkg.Dotnet
		case "github-action", "erlang":
			// we don't want to error out on this, but grype at this version does not support these ecosystems
			return nil, errSkip
		default:
			return nil, fmt.Errorf("unable to determine grype namespace for enterprise namespace=%s", group)
		}
	}

	ns, err := namespace.FromString(fmt.Sprintf("github:language:%s", string(syftLanguage)))

	if err != nil {
		return nil, err
	}

	return ns, nil
}

func Transform(vulnerability unmarshal.GitHubAdvisory) ([]data.Entry, error) {
	var allVulns []db.Vulnerability

	// Exclude entries marked as withdrawn
	if vulnerability.Advisory.Withdrawn != "" {
		return nil, nil
	}

	// TODO: stop capturing record source in the vulnerability metadata record (now that feed groups are not real)
	recordSource := fmt.Sprintf("github:%s", vulnerability.Advisory.Namespace)

	grypeNamespace, err := buildGrypeNamespace(vulnerability.Advisory.Namespace)
	if err != nil {
		if errors.Is(err, errSkip) {
			return nil, nil
		}
		return nil, err
	}

	entryNamespace := grypeNamespace.String()

	// there may be multiple packages indicated within the FixedIn field, we should make
	// separate vulnerability entries (one for each name|namespaces combo) while merging
	// constraint ranges as they are found.
	for idx, fixedInEntry := range vulnerability.Advisory.FixedIn {
		constraint := versionutil.EnforceSemVerConstraint(fixedInEntry.Range)

		var versionFormat string
		switch entryNamespace {
		case "github:language:python":
			versionFormat = "python"
		default:
			versionFormat = "unknown"
		}

		// create vulnerability entry
		allVulns = append(allVulns, db.Vulnerability{
			ID:                     vulnerability.Advisory.GhsaID,
			VersionConstraint:      constraint,
			VersionFormat:          versionFormat,
			RelatedVulnerabilities: getRelatedVulnerabilities(vulnerability),
			PackageName:            grypeNamespace.Resolver().Normalize(fixedInEntry.Name),
			Namespace:              entryNamespace,
			Fix:                    getFix(vulnerability, idx),
		})
	}

	// create vulnerability metadata entry (a single entry keyed off of the vulnerability ID)
	metadata := db.VulnerabilityMetadata{
		ID:           vulnerability.Advisory.GhsaID,
		DataSource:   vulnerability.Advisory.URL,
		Namespace:    entryNamespace,
		RecordSource: recordSource,
		Severity:     vulnerability.Advisory.Severity,
		URLs:         []string{vulnerability.Advisory.URL},
		Description:  vulnerability.Advisory.Summary,
		Cvss:         getCvss(vulnerability),
	}

	return transformers.NewEntries(allVulns, metadata), nil
}

func getFix(entry unmarshal.GitHubAdvisory, idx int) db.Fix {
	fixedInEntry := entry.Advisory.FixedIn[idx]

	var fixedInVersions []string
	fixedInVersion := versionutil.CleanFixedInVersion(fixedInEntry.Identifier)
	if fixedInVersion != "" {
		fixedInVersions = append(fixedInVersions, fixedInVersion)
	}

	fixState := db.NotFixedState
	if len(fixedInVersions) > 0 {
		fixState = db.FixedState
	}

	return db.Fix{
		Versions: fixedInVersions,
		State:    fixState,
	}
}

func getRelatedVulnerabilities(entry unmarshal.GitHubAdvisory) []db.VulnerabilityReference {
	vulns := make([]db.VulnerabilityReference, len(entry.Advisory.CVE))
	for idx, cve := range entry.Advisory.CVE {
		vulns[idx] = db.VulnerabilityReference{
			ID:        cve,
			Namespace: "nvd:cpe",
		}
	}
	return vulns
}

func getCvss(entry unmarshal.GitHubAdvisory) (cvss []db.Cvss) {
	if entry.Advisory.CVSS == nil {
		return cvss
	}

	cvss = append(cvss, db.Cvss{
		Version: entry.Advisory.CVSS.Version,
		Vector:  entry.Advisory.CVSS.VectorString,
		Metrics: db.NewCvssMetrics(
			entry.Advisory.CVSS.BaseMetrics.BaseScore,
			entry.Advisory.CVSS.BaseMetrics.ExploitabilityScore,
			entry.Advisory.CVSS.BaseMetrics.ImpactScore,
		),
		VendorMetadata: transformers.VendorBaseMetrics{
			BaseSeverity: entry.Advisory.CVSS.BaseMetrics.BaseSeverity,
			Status:       entry.Advisory.CVSS.Status,
		},
	})

	return cvss
}
