package unmarshal

import (
	"fmt"
	"io"
	"sort"
	"strings"
	"unicode"

	"github.com/anchore/grype/grype/version"
)

type OSFixedIn struct {
	Module         *string `json:"Module,omitempty"`
	Name           string  `json:"Name"`
	NamespaceName  string  `json:"NamespaceName"`
	VendorAdvisory struct {
		AdvisorySummary []struct {
			ID   string `json:"ID"`
			Link string `json:"Link"`
		} `json:"AdvisorySummary"`
		NoAdvisory bool `json:"NoAdvisory"`
	} `json:"VendorAdvisory"`
	Version         string `json:"Version"`
	VersionFormat   string `json:"VersionFormat"`
	VulnerableRange string `json:"VulnerableRange"`
	Available       struct {
		Date string `json:"Date,omitempty"`
		Kind string `json:"Kind,omitempty"`
	} `json:"Available,omitempty"`
}

type OSFixedIns []OSFixedIn

type OSVulnerability struct {
	Vulnerability struct {
		CVSS []struct {
			BaseMetrics struct {
				BaseScore           float64 `json:"base_score"`
				BaseSeverity        string  `json:"base_severity"`
				ExploitabilityScore float64 `json:"exploitability_score"`
				ImpactScore         float64 `json:"impact_score"`
			} `json:"base_metrics"`
			Status       string `json:"status"`
			VectorString string `json:"vector_string"`
			Version      string `json:"version"`
		} `json:"CVSS"`
		Description string `json:"Description"`
		FixedIn     OSFixedIns
		Link        string `json:"Link"`
		Metadata    struct {
			Issued  string `json:"Issued"`
			Updated string `json:"Updated"`
			RefID   string `json:"RefId"`
			CVE     []struct {
				Name string `json:"Name"`
				Link string `json:"Link"`
			} `json:"CVE"`
			NVD struct {
				CVSSv2 struct {
					Score   float64 `json:"Score"`
					Vectors string  `json:"Vectors"`
				} `json:"CVSSv2"`
			} `json:"NVD"`
		} `json:"Metadata"`
		Name          string `json:"Name"`
		NamespaceName string `json:"NamespaceName"`
		Severity      string `json:"Severity"`
	} `json:"Vulnerability"`
}

func (o OSVulnerability) IsEmpty() bool {
	return o.Vulnerability.Name == ""
}

func OSVulnerabilityEntries(reader io.Reader) ([]OSVulnerability, error) {
	return unmarshalSingleOrMulti[OSVulnerability](reader)
}

// FilterToHighestModularity returns a new distinct set of fixes, keeping only the highest version module fix.
// In cases where there is no modularity the fix is kept.
//

func (fixes OSFixedIns) FilterToHighestModularity() OSFixedIns {
	if len(fixes) < 2 {
		return fixes
	}

	type moduleFix struct {
		constraint version.Constraint
		fix        OSFixedIn
	}

	var keep []OSFixedIn
	moduleHighestFixes := make(map[string]moduleFix)

	for _, f := range fixes {
		validModule, moduleName, v, c := moduleNameAndVersion(f.Module)
		if !validModule {
			keep = append(keep, f)
			continue
		}

		k := fmt.Sprintf("%s|%s|%s", f.Name, f.NamespaceName, moduleName)

		if m, exists := moduleHighestFixes[k]; exists {
			satisfied, err := m.constraint.Satisfied(v)
			if err != nil {
				keep = append(keep, f)
				continue
			}
			if !satisfied {
				continue
			}
		}
		moduleHighestFixes[k] = moduleFix{
			constraint: *c,
			fix:        f,
		}
	}

	// To ensure stable output ordering for tests
	var orderedKeys []string
	for k := range moduleHighestFixes {
		orderedKeys = append(orderedKeys, k)
	}
	sort.Strings(orderedKeys)

	for _, k := range orderedKeys {
		keep = append(keep, moduleHighestFixes[k].fix)
	}

	return keep
}

func moduleNameAndVersion(module *string) (bool, string, *version.Version, *version.Constraint) {
	if module == nil || *module == "" {
		return false, "", nil, nil
	}

	moduleComponents := strings.Split(*module, ":")

	if len(moduleComponents) < 2 {
		return false, "", nil, nil
	}

	moduleName := strings.Join(moduleComponents[0:len(moduleComponents)-1], ":")
	moduleVersion := moduleComponents[len(moduleComponents)-1]
	isPotentiallyVersionedModule := len(moduleVersion) > 0 && unicode.IsDigit(rune(moduleVersion[0]))

	v, c := moduleVersionConstraint(moduleVersion)
	if v == nil || c == nil {
		return false, "", nil, nil
	}

	return isPotentiallyVersionedModule, moduleName, v, c
}

func moduleVersionConstraint(moduleVersion string) (*version.Version, *version.Constraint) {
	v := version.New(moduleVersion, version.UnknownFormat)

	if v == nil {
		return nil, nil
	}

	c := version.MustGetConstraint(fmt.Sprintf("> %s", moduleVersion), version.UnknownFormat)
	return v, &c
}
