package openvex

import (
	"fmt"
	"sort"
	"strings"

	govex "github.com/openvex/go-vex/pkg/vex"

	"github.com/anchore/grype/grype/db/data"
	"github.com/anchore/grype/grype/db/internal/provider/unmarshal"
	"github.com/anchore/grype/grype/db/provider"
	db "github.com/anchore/grype/grype/db/v6"
	"github.com/anchore/grype/grype/db/v6/build/transformers"
	"github.com/anchore/grype/grype/db/v6/build/transformers/internal"
	"github.com/anchore/grype/grype/version"
	"github.com/anchore/packageurl-go"
	syftPkg "github.com/anchore/syft/syft/pkg"
)

func AnnotatedTransform(wrapper unmarshal.AnnotatedOpenVEXVulnerability, state provider.State) ([]data.Entry, error) {
	return transform(wrapper.Document, state, wrapper.Fixes)
}

func Transform(vulnerability unmarshal.OpenVEXVulnerability, state provider.State) ([]data.Entry, error) {
	return transform(vulnerability, state, nil)
}

func transform(vulnerability unmarshal.OpenVEXVulnerability, state provider.State, fixes []unmarshal.AnnotatedOpenVEXFix) ([]data.Entry, error) {
	name := getName(&vulnerability)
	vulnHandle := db.VulnerabilityHandle{
		Name:          name,
		Status:        db.VulnerabilityActive,
		PublishedDate: vulnerability.Timestamp,
		ModifiedDate:  vulnerability.LastUpdated,
		ProviderID:    state.Provider,
		Provider:      provider.Model(state),
		BlobValue: &db.VulnerabilityBlob{
			ID:          name,
			Assigners:   nil,
			Description: vulnerability.Vulnerability.Description,
			References:  getReferences(&vulnerability),
			Aliases:     getAliases(&vulnerability),
		},
	}
	pkgs, err := getPackageHandles(&vulnerability, fixes)
	if err != nil {
		return nil, err
	}
	in := []any{vulnHandle}
	in = append(in, pkgs...)
	return transformers.NewEntries(in...), nil
}

// getPackageHandles for all products in this advisory
func getPackageHandles(vuln *unmarshal.OpenVEXVulnerability, fixes []unmarshal.AnnotatedOpenVEXFix) ([]any, error) {
	if len(vuln.Products) == 0 {
		return nil, nil
	}

	fixesByProduct := make(map[string][]unmarshal.AnnotatedOpenVEXFix)
	for _, fix := range fixes {
		fixesByProduct[fix.Product] = append(fixesByProduct[fix.Product], fix)
	}

	var aphs []db.AffectedPackageHandle
	var uaphs []db.UnaffectedPackageHandle
	for _, product := range vuln.Products {
		aph, uph, err := getPackageHandle(&product, vuln, fixesByProduct[product.Identifiers[govex.PURL]])
		if err != nil {
			return nil, err
		}
		aphs = append(aphs, aph...)
		uaphs = append(uaphs, uph...)
	}

	sort.Sort(internal.ByAffectedPackage(aphs))
	sort.Sort(internal.ByUnaffectedPackage(uaphs))

	var all []any
	for i := range aphs {
		all = append(all, aphs[i])
	}
	for i := range uaphs {
		all = append(all, uaphs[i])
	}

	return all, nil
}

// getPackageHandle for a single product
//
// OpenVEX defines product via:
//
//	Component {
//	  Identifiers: {
//	    PURLIdentifierType: pkg:type/name@version
//	  }
//	}
func getPackageHandle(product *govex.Product, vuln *unmarshal.OpenVEXVulnerability, fixes []unmarshal.AnnotatedOpenVEXFix) (aphs []db.AffectedPackageHandle, uphs []db.UnaffectedPackageHandle, err error) {
	if product == nil || vuln == nil {
		return nil, nil, fmt.Errorf("getAffectedPackage params cannot be nil")
	}
	purl, err := getPURL(product)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to parse purl %s: %w", purl, err)
	}

	pkg := &db.Package{
		Ecosystem: string(syftPkg.TypeFromPURL(purl.String())),
		Name:      purl.Name,
	}

	aliases := []string{getName(vuln)}
	aliases = append(aliases, getAliases(vuln)...)

	switch vuln.Status {
	case govex.StatusAffected:
		aphs = append(aphs, db.AffectedPackageHandle{
			Package:   pkg,
			BlobValue: getPackageBlob(aliases, purl.Version, purl.Type, "", fixes),
		})
	case govex.StatusNotAffected:
		uphs = append(uphs, db.UnaffectedPackageHandle{
			Package:   pkg,
			BlobValue: getPackageBlob(aliases, purl.Version, purl.Type, db.NotAffectedFixStatus, fixes),
		})
	case govex.StatusFixed:
		uphs = append(uphs, db.UnaffectedPackageHandle{
			Package:   pkg,
			BlobValue: getPackageBlob(aliases, purl.Version, purl.Type, db.FixedStatus, fixes),
		})
	default:
		err = fmt.Errorf("invalid vuln states %s", vuln.Status)
	}
	return aphs, uphs, err
}

// getPURL from either ID field or identifiers
func getPURL(product *govex.Product) (purl *packageurl.PackageURL, err error) {
	if p, ok := product.Identifiers[govex.PURL]; ok {
		purl, err := packageurl.FromString(p)
		if err != nil {
			return nil, fmt.Errorf("failed to parse purl %s: %w", p, err)
		}
		return &purl, nil
	}
	if product.ID != "" {
		purl, err := packageurl.FromString(product.ID)
		if err != nil {
			return nil, err
		}
		return &purl, nil
	}
	return nil, fmt.Errorf("invalid product: %v", product)
}

func getAliases(vuln *unmarshal.OpenVEXVulnerability) []string {
	ret := make([]string, 0, len(vuln.Vulnerability.Aliases))
	for _, alias := range vuln.Vulnerability.Aliases {
		ret = append(ret, string(alias))
	}
	return ret
}

func getName(vuln *unmarshal.OpenVEXVulnerability) string {
	return string(vuln.Vulnerability.Name)
}

func getReferences(vuln *unmarshal.OpenVEXVulnerability) []db.Reference {
	refs := []db.Reference{
		{
			URL: getName(vuln),
		},
	}
	return refs
}

func getPackageBlob(aliases []string, ver string, ty string, fixState db.FixStatus, fixes []unmarshal.AnnotatedOpenVEXFix) *db.PackageBlob {
	var fix *db.Fix
	if fixState != "" {
		fix = &db.Fix{
			State: fixState,
		}

		canExpressFixVersion := ver != "" && fixState == db.FixedStatus
		if canExpressFixVersion {
			// only express a fix version if we have a version and the state is "fixed"
			fix.Version = ver
		}

		canExpressFixDetail := len(fixes) > 0 && canExpressFixVersion
		var detail *db.FixDetail
		if canExpressFixDetail {
			time := internal.ParseTime(fixes[0].Available.Date)
			if time != nil && !time.IsZero() {
				detail = &db.FixDetail{
					Available: &db.FixAvailability{
						Date: time,
						Kind: fixes[0].Available.Kind,
					},
				}
			}
		}

		fix.Detail = detail
	}

	return &db.PackageBlob{
		CVEs: aliases,
		Ranges: []db.Range{
			{
				Version: db.Version{
					Type:       strings.ToLower(version.ParseFormat(ty).String()),
					Constraint: fmt.Sprintf("= %s", ver),
				},
				Fix: fix,
			},
		},
	}
}
