package vulns

import (
	"slices"
	"sort"
	"strings"

	"github.com/google/osv-scalibr/semantic"
	"github.com/google/osv-scanner/v2/internal/cmdlogger"
	"github.com/google/osv-scanner/v2/internal/imodels"
	"github.com/google/osv-scanner/v2/internal/imodels/ecosystem"
	"github.com/ossf/osv-schema/bindings/go/osvschema"
)

func eventVersion(e osvschema.Event) string {
	if e.Introduced != "" {
		return e.Introduced
	}

	if e.Fixed != "" {
		return e.Fixed
	}

	if e.Limit != "" {
		return e.Limit
	}

	if e.LastAffected != "" {
		return e.LastAffected
	}

	return ""
}

func rangeContainsVersion(ar osvschema.Range, pkg imodels.PackageInfo) bool {
	if ar.Type != osvschema.RangeEcosystem && ar.Type != osvschema.RangeSemVer {
		return false
	}
	// todo: we should probably warn here
	if len(ar.Events) == 0 {
		return false
	}

	vp := semantic.MustParse(pkg.Version(), string(pkg.Ecosystem().Ecosystem))

	sort.Slice(ar.Events, func(i, j int) bool {
		a := ar.Events[i]
		b := ar.Events[j]

		if a.Introduced == "0" {
			return true
		}

		if b.Introduced == "0" {
			return false
		}

		// Ignore errors as we assume the version is correct
		order, _ := semantic.MustParse(eventVersion(a), string(pkg.Ecosystem().Ecosystem)).CompareStr((eventVersion(b)))

		return order < 0
	})

	var affected bool
	for _, e := range ar.Events {
		if affected {
			if e.Fixed != "" {
				order, _ := vp.CompareStr(e.Fixed)
				affected = order < 0
			} else if e.LastAffected != "" {
				order, _ := vp.CompareStr(e.LastAffected)
				affected = e.LastAffected == pkg.Version() || order <= 0
			}
		} else if e.Introduced != "" {
			order, _ := vp.CompareStr(e.Introduced)
			affected = e.Introduced == "0" || order >= 0
		}
	}

	return affected
}

// rangeAffectsVersion checks if the given version is within the range
// specified by the events of any "Ecosystem" or "Semver" type ranges
func rangeAffectsVersion(a []osvschema.Range, pkg imodels.PackageInfo) bool {
	for _, r := range a {
		if r.Type != osvschema.RangeEcosystem && r.Type != osvschema.RangeSemVer {
			return false
		}
		if rangeContainsVersion(r, pkg) {
			return true
		}
	}

	return false
}

// func isAliasOfID(v osvschema.Vulnerability, id string) bool {
// 	for _, alias := range v.Aliases {
// 		if alias == id {
// 			return true
// 		}
// 	}

// 	return false
// }

// func isAliasOf(v osvschema.Vulnerability, vulnerability osvschema.Vulnerability) bool {
// 	for _, alias := range vulnerability.Aliases {
// 		if v.ID == alias || isAliasOfID(v, alias) {
// 			return true
// 		}
// 	}

// 	return false
// }

func AffectsEcosystem(v osvschema.Vulnerability, ecosystemAffected ecosystem.Parsed) bool {
	for _, affected := range v.Affected {
		if ecosystem.MustParse(affected.Package.Ecosystem).Equal(ecosystemAffected) {
			return true
		}
	}

	return false
}

func IsAffected(v osvschema.Vulnerability, pkg imodels.PackageInfo) bool {
	for _, affected := range v.Affected {
		// Assume vulnerability has already been validated
		if ecosystem.MustParse(affected.Package.Ecosystem).Equal(pkg.Ecosystem()) &&
			affected.Package.Name == pkg.Name() {
			if len(affected.Ranges) == 0 && len(affected.Versions) == 0 {
				cmdlogger.Warnf("%s does not have any ranges or versions - this is probably a mistake!", v.ID)

				continue
			}

			if slices.Contains(affected.Versions, pkg.Version()) {
				return true
			}

			if rangeAffectsVersion(affected.Ranges, pkg) {
				return true
			}

			// if a package does not have a version, assume it is vulnerable
			// as false positives are better than false negatives here
			if pkg.Version() == "" {
				return true
			}
		}
	}

	return false
}

// GetFixedVersions returns a map of fixed versions for each package, or a map of empty slices if no fixed versions are available
func GetFixedVersions(v osvschema.Vulnerability) map[osvschema.Package][]string {
	output := map[osvschema.Package][]string{}
	for _, a := range v.Affected {
		packageKey := a.Package
		packageKey.Purl = ""
		for _, r := range a.Ranges {
			for _, e := range r.Events {
				if e.Fixed != "" {
					output[packageKey] = append(output[packageKey], e.Fixed)
					if strings.Contains(packageKey.Ecosystem, ":") {
						packageKey.Ecosystem = strings.Split(packageKey.Ecosystem, ":")[0]
					}
					output[packageKey] = append(output[packageKey], e.Fixed)
				}
			}
		}
	}

	return output
}
