package models

import (
	"sort"

	"github.com/anchore/grype/grype/version"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/grype/internal/log"
)

type Vulnerability struct {
	VulnerabilityMetadata
	Fix        Fix        `json:"fix"`
	Advisories []Advisory `json:"advisories"`
	Risk       float64    `json:"risk"`
}

type Fix struct {
	Versions  []string       `json:"versions"`
	State     string         `json:"state"`
	Available []FixAvailable `json:"available,omitempty"`
}

type FixAvailable struct {
	Version string `json:"version"`
	Date    string `json:"date"`
	Kind    string `json:"kind,omitempty"`
}

type Advisory struct {
	ID   string `json:"id"`
	Link string `json:"link"`
}

func NewVulnerability(vuln vulnerability.Vulnerability, metadata *vulnerability.Metadata, versionFormat version.Format) Vulnerability {
	if metadata == nil {
		return Vulnerability{
			VulnerabilityMetadata: NewVulnerabilityMetadata(vuln.ID, vuln.Namespace, metadata),
		}
	}

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

	fixedInVersions := vuln.Fix.Versions
	if fixedInVersions == nil {
		// always allocate collections
		fixedInVersions = make([]string, 0)
	}

	return Vulnerability{
		VulnerabilityMetadata: NewVulnerabilityMetadata(vuln.ID, vuln.Namespace, metadata),
		Fix: Fix{
			Versions:  sortVersions(fixedInVersions, versionFormat),
			State:     string(vuln.Fix.State),
			Available: getFixAvailable(vuln.Fix.Available),
		},
		Advisories: advisories,
		Risk:       metadata.RiskScore(),
	}
}

func getFixAvailable(fixesAvailable []vulnerability.FixAvailable) []FixAvailable {
	if len(fixesAvailable) == 0 {
		return nil
	}

	var results []FixAvailable
	for _, fix := range fixesAvailable {
		if fix.Date.IsZero() {
			continue
		}
		f := FixAvailable{
			Version: fix.Version,
			Date:    fix.Date.Format("2006-01-02"), // just extract the
			Kind:    fix.Kind,
		}
		results = append(results, f)
	}

	return results
}

func sortVersions(fixedVersions []string, format version.Format) []string {
	if len(fixedVersions) <= 1 {
		return fixedVersions
	}

	// first, create Version objects from strings (only once)
	versionObjs := make([]*version.Version, 0, len(fixedVersions))
	var invalidVersions []string
	for _, vStr := range fixedVersions {
		v := version.New(vStr, format)
		err := v.Validate()
		if err != nil {
			log.WithFields("version", vStr, "error", err).Trace("error parsing version, skipping")
			invalidVersions = append(invalidVersions, vStr)
			continue
		}
		versionObjs = append(versionObjs, v)
	}

	// sort the Version objects
	sort.Slice(versionObjs, func(i, j int) bool {
		comparison, err := versionObjs[i].Compare(versionObjs[j])
		if err != nil {
			log.WithFields("error", err).Trace("error comparing versions")
			return false
		}
		return comparison < 0
	})

	// convert back to strings
	var result []string
	for _, v := range versionObjs {
		result = append(result, v.Raw)
	}

	result = append(result, invalidVersions...)

	return result
}
