// Package osvscanner provides the main logic for the OSV-Scanner.
package osvscanner

import (
	"fmt"

	"github.com/google/osv-scalibr/extractor"
	"github.com/google/osv-scanner/v2/internal/cmdlogger"
	"github.com/google/osv-scanner/v2/internal/config"
	"github.com/google/osv-scanner/v2/internal/imodels"
	"github.com/google/osv-scanner/v2/internal/imodels/results"
	"github.com/google/osv-scanner/v2/pkg/models"
	"github.com/ossf/osv-schema/bindings/go/osvconstants"
	"github.com/ossf/osv-schema/bindings/go/osvschema"
)

// filterUnscannablePackages removes packages that don't have enough information to be scanned or
// are not a supported ecosystem, and returns the list of removed packages (if --all-packages flag is passed in)
// e,g, local packages that specified by path
func filterUnscannablePackages(scanResults *results.ScanResults, actions ScannerActions) []*extractor.Package {
	packageResults := make([]*extractor.Package, 0, len(scanResults.Inventory.Packages))
	filteredPsr := make([]*extractor.Package, 0, len(scanResults.Inventory.Packages))
	for _, psr := range scanResults.Inventory.Packages {
		switch {
		// If **none** of the cases match, skip this package since it's not scannable
		case !imodels.Ecosystem(psr).IsEmpty() && imodels.Name(psr) != "" && imodels.Version(psr) != "":
		case imodels.Commit(psr) != "":
		default:
			if actions.ShowAllPackages {
				filteredPsr = append(filteredPsr, psr)
			}

			continue
		}

		switch {
		// If **any** of the following cases are true, skip this package
		case imodels.Ecosystem(psr).Ecosystem == osvconstants.EcosystemMaven && imodels.Name(psr) == "unknown", // Is Maven with package name unknown
			imodels.Ecosystem(psr).GetValidity() != nil && !imodels.Ecosystem(psr).IsEmpty(): // Is invalid and not empty
			if actions.ShowAllPackages {
				filteredPsr = append(filteredPsr, psr)
			}

			continue
		// Short commit hashes (< 40 hex chars) are rejected by the OSV API with
		// "Invalid hash". Skip them with a warning rather than aborting the scan.
		case imodels.Commit(psr) != "" && len(imodels.Commit(psr)) < 40:
			cmdlogger.Warnf("Skipping %s: short commit hash %q cannot be queried; OSV API requires a full 40-character SHA.", imodels.Name(psr), imodels.Commit(psr))

			if actions.ShowAllPackages {
				filteredPsr = append(filteredPsr, psr)
			}

			continue
		}

		packageResults = append(packageResults, psr)
	}

	if len(packageResults) != len(scanResults.Inventory.Packages) {
		cmdlogger.Infof("Filtered %d local/unscannable package/s from the scan.", len(scanResults.Inventory.Packages)-len(packageResults))
	}

	scanResults.Inventory.Packages = packageResults

	return filteredPsr
}

// filterNonContainerRelevantPackages removes packages that are not relevant when doing container scanning
func filterNonContainerRelevantPackages(scanResults *results.ScanResults) {
	packageResults := make([]*extractor.Package, 0, len(scanResults.Inventory.Packages))
	for _, psr := range scanResults.Inventory.Packages {
		// Almost all packages with linux as a SourceName are kernel packages
		// which does not apply within a container, as containers use the host's kernel
		if imodels.Name(psr) == "linux" {
			continue
		}

		packageResults = append(packageResults, psr)
	}

	if len(packageResults) != len(scanResults.Inventory.Packages) {
		cmdlogger.Infof("Filtered %d non container relevant package/s from the scan.", len(scanResults.Inventory.Packages)-len(packageResults))
	}

	scanResults.Inventory.Packages = packageResults
}

// filterIgnoredPackages removes ignore scanned packages according to config. Returns filtered scanned packages.
func filterIgnoredPackages(scanResults *results.ScanResults) {
	configManager := &scanResults.ConfigManager

	out := make([]*extractor.Package, 0, len(scanResults.Inventory.Packages))
	for _, psr := range scanResults.Inventory.Packages {
		configToUse := configManager.Get(imodels.Location(psr))

		if ignore, ignoreLine := configToUse.ShouldIgnorePackage(psr); ignore {
			pkgString := fmt.Sprintf("%s/%s/%s", imodels.Ecosystem(psr).String(), imodels.Name(psr), imodels.Version(psr))

			reason := ignoreLine.Reason
			if reason == "" {
				reason = "(no reason given)"
			}
			cmdlogger.Infof("Package %s has been filtered out because: %s", pkgString, reason)

			continue
		}
		out = append(out, psr)
	}

	if len(out) != len(scanResults.Inventory.Packages) {
		cmdlogger.Infof("Filtered %d ignored package/s from the scan.", len(scanResults.Inventory.Packages)-len(out))
	}

	scanResults.Inventory.Packages = out
}

// Filters results according to config, preserving order. Returns total number of vulnerabilities removed.
func filterResults(vulnResults *models.VulnerabilityResults, configManager *config.Manager, allPackages bool) int {
	removedCount := 0
	newResults := []models.PackageSource{} // Want 0 vulnerabilities to show in JSON as an empty list, not null.
	for _, pkgSrc := range vulnResults.Results {
		configToUse := configManager.Get(pkgSrc.Source.Path)
		var newPackages []models.PackageVulns
		for _, pkgVulns := range pkgSrc.Packages {
			newVulns := filterPackageVulns(pkgVulns, configToUse)
			removedCount += len(pkgVulns.Vulnerabilities) - len(newVulns.Vulnerabilities)
			if allPackages || len(newVulns.Vulnerabilities) > 0 || len(pkgVulns.LicenseViolations) > 0 || pkgVulns.Package.Deprecated {
				newPackages = append(newPackages, newVulns)
			}
		}
		// Don't want to include the package source at all if there are no vulns.
		if len(newPackages) > 0 {
			pkgSrc.Packages = newPackages
			newResults = append(newResults, pkgSrc)
		}
	}
	vulnResults.Results = newResults

	return removedCount
}

// Filters package-grouped vulnerabilities according to config, preserving ordering. Returns filtered package vulnerabilities.
func filterPackageVulns(pkgVulns models.PackageVulns, configToUse config.Config) models.PackageVulns {
	ignoredVulns := map[string]struct{}{}

	// Iterate over groups first to remove all aliases of ignored vulnerabilities.
	var newGroups []models.GroupInfo
	for _, group := range pkgVulns.Groups {
		ignore := false
		for _, id := range group.Aliases {
			var ignoreLine *config.IgnoreEntry
			if ignore, ignoreLine = configToUse.ShouldIgnore(id); ignore {
				for _, id := range group.Aliases {
					ignoredVulns[id] = struct{}{}
				}

				reason := ignoreLine.Reason

				if reason == "" {
					reason = "(no reason given)"
				}

				// NB: This only prints the first reason encountered in all the aliases.
				switch len(group.Aliases) {
				case 1:
					cmdlogger.Infof("%s has been filtered out because: %s", ignoreLine.ID, reason)
				case 2:
					cmdlogger.Infof("%s and 1 alias have been filtered out because: %s", ignoreLine.ID, reason)
				default:
					cmdlogger.Infof("%s and %d aliases have been filtered out because: %s", ignoreLine.ID, len(group.Aliases)-1, reason)
				}

				ignoreLine.MarkAsUsed()

				break
			}
		}
		if !ignore {
			newGroups = append(newGroups, group)
		}
	}

	var newVulns []*osvschema.Vulnerability
	if len(newGroups) > 0 { // If there are no groups left then there would be no vulnerabilities.
		for _, vuln := range pkgVulns.Vulnerabilities {
			if _, filtered := ignoredVulns[vuln.GetId()]; !filtered {
				newVulns = append(newVulns, vuln)
			}
		}
	}

	// Passed by value. We don't want to alter the original PackageVulns.
	pkgVulns.Groups = newGroups
	pkgVulns.Vulnerabilities = newVulns

	return pkgVulns
}
