package grype

import (
	"context"
	"errors"
	"fmt"
	"runtime/debug"
	"slices"
	"strings"
	"time"

	"github.com/wagoodman/go-partybus"
	"github.com/wagoodman/go-progress"

	"github.com/anchore/grype/grype/event"
	"github.com/anchore/grype/grype/event/monitor"
	"github.com/anchore/grype/grype/grypeerr"
	"github.com/anchore/grype/grype/match"
	"github.com/anchore/grype/grype/matcher/stock"
	"github.com/anchore/grype/grype/pkg"
	"github.com/anchore/grype/grype/vex"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/grype/internal/bus"
	"github.com/anchore/grype/internal/log"
	syftPkg "github.com/anchore/syft/syft/pkg"
)

const (
	branch = "├──"
	leaf   = "└──"
)

// AlertsConfig controls which alerts are tracked and reported during vulnerability matching.
type AlertsConfig struct {
	// EnableEOLDistroWarnings enables tracking packages from end-of-life distros
	EnableEOLDistroWarnings bool
}

type VulnerabilityMatcher struct {
	VulnerabilityProvider vulnerability.Provider
	ExclusionProvider     match.ExclusionProvider
	Matchers              []match.Matcher
	IgnoreRules           []match.IgnoreRule
	FailSeverity          *vulnerability.Severity
	NormalizeByCVE        bool
	VexProcessor          *vex.Processor
	Alerts                AlertsConfig

	// tracked packages with distro issues (populated during FindMatches)
	eolDistroPackages     []pkg.Package
	distroDetectionFailed bool
}

func (m *VulnerabilityMatcher) FailAtOrAboveSeverity(severity *vulnerability.Severity) *VulnerabilityMatcher {
	m.FailSeverity = severity
	return m
}

func (m *VulnerabilityMatcher) WithMatchers(matchers []match.Matcher) *VulnerabilityMatcher {
	m.Matchers = matchers
	return m
}

func (m *VulnerabilityMatcher) WithIgnoreRules(ignoreRules []match.IgnoreRule) *VulnerabilityMatcher {
	m.IgnoreRules = ignoreRules
	return m
}

// DistroDetectionFailed returns true if distro detection failed during scanning
// (linux release info was present but distro type could not be determined).
func (m *VulnerabilityMatcher) DistroDetectionFailed() bool {
	return m.distroDetectionFailed
}

// EOLDistroPackages returns packages from distros that have reached end-of-life.
func (m *VulnerabilityMatcher) EOLDistroPackages() []pkg.Package {
	return m.eolDistroPackages
}

// FindMatches finds vulnerabilities for the given packages and package context.
// FindMatches does not support context cancellation; for that, use
// FindMatchesContext.
func (m *VulnerabilityMatcher) FindMatches(
	pkgs []pkg.Package,
	pkgContext pkg.Context,
) (remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, err error) {
	return m.FindMatchesContext(context.Background(), pkgs, pkgContext)
}

// FindMatchesContext finds vulnerabilities for the given packages and package
// context, and supports context cancellation.
func (m *VulnerabilityMatcher) FindMatchesContext(
	ctx context.Context,
	pkgs []pkg.Package,
	pkgContext pkg.Context,
) (remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, err error) {
	progressMonitor := trackMatcher(len(pkgs))

	// capture distro detection failure from pkgContext for alerting
	m.distroDetectionFailed = pkgContext.DistroDetectionFailed
	if m.distroDetectionFailed {
		log.Warn("distro detection failed: linux release info was present but distro type could not be determined")
	}

	defer func() {
		progressMonitor.Ignored.Set(int64(len(ignoredMatches)))
		progressMonitor.SetCompleted()
		if err != nil {
			progressMonitor.MatchesDiscovered.SetError(err)
		}
	}()

	remainingMatches, ignoredMatches, err = m.findDBMatches(ctx, pkgs, progressMonitor)
	if err != nil {
		err = fmt.Errorf("unable to find matches against vulnerability database: %w", err)
		return remainingMatches, ignoredMatches, err
	}

	remainingMatches, ignoredMatches, err = m.findVEXMatches(pkgContext, remainingMatches, ignoredMatches, progressMonitor)
	if err != nil {
		err = fmt.Errorf("unable to find matches against VEX sources: %w", err)
		return remainingMatches, ignoredMatches, err
	}

	if m.FailSeverity != nil && hasSeverityAtOrAbove(m.VulnerabilityProvider, *m.FailSeverity, *remainingMatches) {
		err = grypeerr.ErrAboveSeverityThreshold
		return remainingMatches, ignoredMatches, err
	}

	logListSummary(progressMonitor)

	logIgnoredMatches(ignoredMatches)

	return remainingMatches, ignoredMatches, nil
}

func (m *VulnerabilityMatcher) findDBMatches(ctx context.Context, pkgs []pkg.Package, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) {
	var ignoredMatches []match.IgnoredMatch

	log.Trace("finding matches against DB")
	matches, err := m.searchDBForMatches(ctx, pkgs, progressMonitor)
	if err != nil {
		if match.IsFatalError(err) {
			return nil, nil, err
		}

		// other errors returned from matchers during searchDBForMatches were being
		// logged and not returned, so just log them here
		log.WithFields("error", err).Debug("error(s) returned from searchDBForMatches")
	}

	matches, ignoredMatches = m.applyIgnoreRules(matches)

	if m.NormalizeByCVE {
		normalizedMatches := match.NewMatches()
		for originalMatch := range matches.Enumerate() {
			normalizedMatches.Add(m.normalizeByCVE(originalMatch))
		}

		// we apply the ignore rules again in case any of the transformations done during normalization
		// regresses the results (relative to the already applied ignore rules). Why do we additionally apply
		// the ignore rules before normalizing? In case the user has a rule that ignores a non-normalized
		// vulnerability ID, we wantMatches to ensure that the rule is honored.
		originalIgnoredMatches := ignoredMatches
		matches, ignoredMatches = m.applyIgnoreRules(normalizedMatches)
		ignoredMatches = m.mergeIgnoredMatches(originalIgnoredMatches, ignoredMatches)
	}

	return &matches, ignoredMatches, nil
}

func (m *VulnerabilityMatcher) mergeIgnoredMatches(allIgnoredMatches ...[]match.IgnoredMatch) []match.IgnoredMatch {
	var out []match.IgnoredMatch
	for _, ignoredMatches := range allIgnoredMatches {
		for _, ignored := range ignoredMatches {
			if m.NormalizeByCVE {
				ignored.Match = m.normalizeByCVE(ignored.Match)
			}
			out = append(out, ignored)
		}
	}
	return out
}

//nolint:funlen
func (m *VulnerabilityMatcher) searchDBForMatches(
	ctx context.Context,
	packages []pkg.Package,
	progressMonitor *monitorWriter,
) (match.Matches, error) {
	var allMatches []match.Match
	var allIgnorers []match.IgnoreFilter
	matcherIndex, defaultMatcher := newMatcherIndex(m.Matchers)

	if defaultMatcher == nil {
		defaultMatcher = stock.NewStockMatcher(stock.MatcherConfig{UseCPEs: true})
	}

	// reset tracked distro packages
	m.eolDistroPackages = nil

	// setup EOL tracking if enabled
	eolTracker := newEOLTracker(m.Alerts.EnableEOLDistroWarnings, m.VulnerabilityProvider)

	var matcherErrs []error
	for _, p := range packages {
		progressMonitor.PackagesProcessed.Increment()
		log.WithFields("package", displayPackage(p)).Trace("searching for vulnerability matches")

		// track EOL distro packages
		if eolTracker.checkAndTrack(p) {
			m.eolDistroPackages = append(m.eolDistroPackages, p)
		}

		matchAgainst, ok := matcherIndex[p.Type]
		if !ok {
			matchAgainst = []match.Matcher{defaultMatcher}
		}
		for _, theMatcher := range matchAgainst {
			if err := ctx.Err(); err != nil {
				return match.Matches{}, err
			}

			matches, ignorers, err := callMatcherSafely(theMatcher, m.VulnerabilityProvider, p)
			if err != nil {
				if match.IsFatalError(err) {
					return match.Matches{}, err
				}

				log.WithFields("error", err, "package", displayPackage(p)).Warn("matcher returned error")
				matcherErrs = append(matcherErrs, err)
			}

			allIgnorers = append(allIgnorers, ignorers...)

			// Filter out matches based on records in the database exclusion table and hard-coded rules
			filtered, dropped := match.ApplyExplicitIgnoreRules(m.ExclusionProvider, match.NewMatches(matches...))

			additionalMatches := filtered.Sorted()
			logPackageMatches(p, additionalMatches)
			logExplicitDroppedPackageMatches(p, dropped)
			allMatches = append(allMatches, additionalMatches...)

			progressMonitor.MatchesDiscovered.Add(int64(len(additionalMatches)))

			// note: there is a difference between "ignore" and "dropped" matches.
			// ignored: matches that are filtered out due to user-provided ignore rules
			// dropped: matches that are filtered out due to hard-coded rules
			updateVulnerabilityList(progressMonitor, additionalMatches, nil, dropped, m.VulnerabilityProvider)
		}
	}

	// apply ignores based on matchers returning ignore rules
	startTime := time.Now()
	filtered, dropped := match.ApplyIgnoreFilters(allMatches, ignoredMatchFilter(allIgnorers))
	logIgnoredMatches(dropped)
	log.Debugf("took %v to process %v vulns with %v ignores", time.Since(startTime), len(allMatches), len(allIgnorers))

	// get deduplicated set of matches
	res := match.NewMatches(filtered...)

	// update the total discovered matches after removing all duplicates and ignores
	progressMonitor.MatchesDiscovered.Set(int64(res.Count()))

	return res, errors.Join(matcherErrs...)
}

func callMatcherSafely(m match.Matcher, vp vulnerability.Provider, p pkg.Package) (matches []match.Match, ignoredMatches []match.IgnoreFilter, err error) {
	// handle individual matcher panics
	defer func() {
		if e := recover(); e != nil {
			err = match.NewFatalError(m.Type(), fmt.Errorf("%v at:\n%s", e, string(debug.Stack())))
		}
	}()
	return m.Match(vp, p)
}

func (m *VulnerabilityMatcher) findVEXMatches(pkgContext pkg.Context, remainingMatches *match.Matches, ignoredMatches []match.IgnoredMatch, progressMonitor *monitorWriter) (*match.Matches, []match.IgnoredMatch, error) {
	if m.VexProcessor == nil {
		log.Trace("no VEX documents provided, skipping VEX matching")
		return remainingMatches, ignoredMatches, nil
	}

	log.Trace("finding matches against available VEX documents")
	matchesAfterVex, ignoredMatchesAfterVex, err := m.VexProcessor.ApplyVEX(&pkgContext, remainingMatches, ignoredMatches)
	if err != nil {
		return nil, nil, fmt.Errorf("unable to find matches against VEX documents: %w", err)
	}

	diffMatches := matchesAfterVex.Diff(*remainingMatches)
	// note: this assumes that the diff can only be additive
	diffIgnoredMatches := ignoredMatchesDiff(ignoredMatchesAfterVex, ignoredMatches)

	updateVulnerabilityList(progressMonitor, diffMatches.Sorted(), diffIgnoredMatches, nil, m.VulnerabilityProvider)

	return matchesAfterVex, ignoredMatchesAfterVex, nil
}

// applyIgnoreRules applies the user-provided ignore rules, splitting ignored matches into a separate set
func (m *VulnerabilityMatcher) applyIgnoreRules(matches match.Matches) (match.Matches, []match.IgnoredMatch) {
	var ignoredMatches []match.IgnoredMatch
	if len(m.IgnoreRules) == 0 {
		return matches, ignoredMatches
	}

	matches, ignoredMatches = match.ApplyIgnoreRules(matches, m.IgnoreRules)

	if count := len(ignoredMatches); count > 0 {
		log.Infof("ignoring %d matches due to user-provided ignore rules", count)
	}
	return matches, ignoredMatches
}

func (m *VulnerabilityMatcher) normalizeByCVE(match match.Match) match.Match {
	if isCVE(match.Vulnerability.ID) {
		return match
	}

	var effectiveCVERecordRefs []vulnerability.Reference
	for _, ref := range match.Vulnerability.RelatedVulnerabilities {
		if isCVE(ref.ID) {
			effectiveCVERecordRefs = append(effectiveCVERecordRefs, ref)
			break
		}
	}

	switch len(effectiveCVERecordRefs) {
	case 0:
		log.WithFields(
			"vuln", match.Vulnerability.ID,
			"package", displayPackage(match.Package),
		).Trace("unable to find CVE record for vulnerability, skipping normalization")
		return match
	case 1:
		break
	default:
		log.WithFields(
			"refs", fmt.Sprintf("%+v", effectiveCVERecordRefs),
			"vuln", match.Vulnerability.ID,
			"package", displayPackage(match.Package),
		).Trace("found multiple CVE records for vulnerability, skipping normalization")
		return match
	}

	ref := effectiveCVERecordRefs[0]

	upstreamMetadata, err := m.VulnerabilityProvider.VulnerabilityMetadata(ref) //nolint:staticcheck // deprecated API still used internally
	if err != nil {
		log.WithFields("id", ref.ID, "namespace", ref.Namespace, "error", err).Warn("unable to fetch effective CVE metadata")
		return match
	}

	if upstreamMetadata == nil {
		return match
	}

	originalRef := vulnerability.Reference{
		ID:        match.Vulnerability.ID,
		Namespace: match.Vulnerability.Namespace,
	}

	match.Vulnerability.ID = upstreamMetadata.ID
	match.Vulnerability.Namespace = upstreamMetadata.Namespace
	match.Vulnerability.RelatedVulnerabilities = []vulnerability.Reference{originalRef}

	return match
}

// ignoreRulesByIndex implements match.IgnoreFilter to filter each matching
// package that overlaps by location and have the same vulnerability ID (CVE)
type ignoreRulesByIndex struct {
	remainingFilters       []match.IgnoreFilter
	locationIgnoreRules    map[string][]match.IgnoreRule
	packageNameIgnoreRules map[string][]match.IgnoreRule
	vulnIDFilters          map[string][]match.IgnoreFilter
}

func (i ignoreRulesByIndex) IgnoreMatch(m match.Match) []match.IgnoreRule {
	if matched := matchIndexed(m, i.vulnIDFilters[m.Vulnerability.ID]); matched != nil {
		return matched
	}
	for _, related := range m.Vulnerability.RelatedVulnerabilities {
		if matched := matchIndexed(m, i.vulnIDFilters[related.ID]); matched != nil {
			return matched
		}
	}
	if matched := matchIndexed(m, i.packageNameIgnoreRules[m.Package.Name]); matched != nil {
		return matched
	}
	for _, l := range m.Package.Locations.ToSlice() {
		if matched := matchIndexed(m, i.locationIgnoreRules[l.RealPath]); matched != nil {
			return matched
		}
	}
	return matchIndexed(m, i.remainingFilters)
}

func matchIndexed[E match.IgnoreFilter, F ~[]E](m match.Match, vulnFilters F) []match.IgnoreRule {
	for _, rule := range vulnFilters {
		if matched := rule.IgnoreMatch(m); matched != nil {
			return matched
		}
	}
	return nil
}

// ignoredMatchFilter creates an ignore filter based on location-based IgnoredMatches to filter out "the same"
// vulnerabilities reported by other matchers based on overlapping file locations
func ignoredMatchFilter(ignores []match.IgnoreFilter) match.IgnoreFilter {
	out := ignoreRulesByIndex{
		locationIgnoreRules:    map[string][]match.IgnoreRule{},
		packageNameIgnoreRules: map[string][]match.IgnoreRule{},
		vulnIDFilters:          map[string][]match.IgnoreFilter{},
	}
	// the returned slice of remaining rules are not location-based rules
	out.remainingFilters = slices.DeleteFunc(ignores, func(ignore match.IgnoreFilter) bool {
		if rule, ok := ignore.(match.IgnoreRule); ok {
			// return true to remove rules handled with index lookups from the remaining filter list
			if rule.Package.Location != "" && !strings.ContainsRune(rule.Package.Location, '*') {
				out.locationIgnoreRules[rule.Package.Location] = append(out.locationIgnoreRules[rule.Package.Location], rule)
				return true
			}
			if rule.Package.Name != "" {
				// this rule is handled with location lookups, remove it from the remaining filter list
				out.packageNameIgnoreRules[rule.Package.Name] = append(out.packageNameIgnoreRules[rule.Package.Name], rule)
				return true
			}
			if rule.Vulnerability != "" && rule.IncludeAliases {
				out.vulnIDFilters[rule.Vulnerability] = append(out.vulnIDFilters[rule.Vulnerability], ignore)
				return true
			}
		}
		if rule, ok := ignore.(match.IgnoreRelatedPackage); ok {
			if rule.VulnerabilityID != "" {
				out.vulnIDFilters[rule.VulnerabilityID] = append(out.vulnIDFilters[rule.VulnerabilityID], ignore)
				return true
			}
		}
		return false
	})
	log.Tracef("ignore filter index remaining filters: %v", len(out.remainingFilters))
	return out
}

func displayPackage(p pkg.Package) string {
	if p.PURL != "" {
		return p.PURL
	}
	ty := p.Type
	if p.Type == "" {
		ty = "unknown"
	}

	return fmt.Sprintf("%s@%s (type=%s)", p.Name, p.Version, ty)
}

func ignoredMatchesDiff(subject []match.IgnoredMatch, other []match.IgnoredMatch) []match.IgnoredMatch {
	// TODO(alex): the downside with this implementation is that it does not account for the same ignored match being
	// ignored for different reasons (the appliedIgnoreRules field).

	otherMap := make(map[match.Fingerprint]struct{})
	for _, a := range other {
		otherMap[a.Fingerprint()] = struct{}{}
	}

	var diff []match.IgnoredMatch
	for _, b := range subject {
		if _, ok := otherMap[b.Fingerprint()]; !ok {
			diff = append(diff, b)
		}
	}

	return diff
}

func newMatcherIndex(matchers []match.Matcher) (map[syftPkg.Type][]match.Matcher, match.Matcher) {
	matcherIndex := make(map[syftPkg.Type][]match.Matcher)
	var defaultMatcher match.Matcher
	for _, m := range matchers {
		if m.Type() == match.StockMatcher {
			defaultMatcher = m
			continue
		}
		for _, t := range m.PackageTypes() {
			if _, ok := matcherIndex[t]; !ok {
				matcherIndex[t] = make([]match.Matcher, 0)
			}

			matcherIndex[t] = append(matcherIndex[t], m)
			log.Tracef("adding matcher: %+v", t)
		}
	}

	return matcherIndex, defaultMatcher
}

func isCVE(id string) bool {
	return strings.HasPrefix(strings.ToLower(id), "cve-")
}

//nolint:staticcheck // MetadataProvider is deprecated but still used internally
func hasSeverityAtOrAbove(store vulnerability.MetadataProvider, severity vulnerability.Severity, matches match.Matches) bool {
	if severity == vulnerability.UnknownSeverity {
		return false
	}
	for m := range matches.Enumerate() {
		metadata, err := store.VulnerabilityMetadata(m.Vulnerability.Reference) //nolint:staticcheck // deprecated API still used internally
		if err != nil {
			continue
		}

		if vulnerability.ParseSeverity(metadata.Severity) >= severity {
			return true
		}
	}
	return false
}

func logListSummary(vl *monitorWriter) {
	log.Infof("found %d vulnerability matches across %d packages", vl.MatchesDiscovered.Current(), vl.PackagesProcessed.Current())
	log.Debugf("  ├── fixed: %d", vl.Fixed.Current())
	log.Debugf("  ├── ignored: %d (due to user-provided rule)", vl.Ignored.Current())
	log.Debugf("  ├── dropped: %d (due to hard-coded correction)", vl.Dropped.Current())
	log.Debugf("  └── matched: %d", vl.MatchesDiscovered.Current())

	var unknownCount int64
	if count, ok := vl.BySeverity[vulnerability.UnknownSeverity]; ok {
		unknownCount = count.Current()
	}
	log.Debugf("      ├── %s: %d", vulnerability.UnknownSeverity.String(), unknownCount)

	allSeverities := vulnerability.AllSeverities()
	for idx, sev := range allSeverities {
		arm := selectArm(idx, len(allSeverities))
		log.Debugf("      %s %s: %d", arm, sev.String(), vl.BySeverity[sev].Current())
	}
}

//nolint:staticcheck // MetadataProvider is deprecated but still used internally
func updateVulnerabilityList(mon *monitorWriter, matches []match.Match, ignores []match.IgnoredMatch, dropped []match.IgnoredMatch, metadataProvider vulnerability.MetadataProvider) {
	for _, m := range matches {
		metadata, err := metadataProvider.VulnerabilityMetadata(m.Vulnerability.Reference) //nolint:staticcheck // deprecated API still used internally
		if err != nil || metadata == nil {
			mon.BySeverity[vulnerability.UnknownSeverity].Increment()
			continue
		}

		sevManualProgress, ok := mon.BySeverity[vulnerability.ParseSeverity(metadata.Severity)]
		if !ok {
			mon.BySeverity[vulnerability.UnknownSeverity].Increment()
			continue
		}
		sevManualProgress.Increment()

		if m.Vulnerability.Fix.State == vulnerability.FixStateFixed {
			mon.Fixed.Increment()
		}
	}

	mon.Ignored.Add(int64(len(ignores)))
	mon.Dropped.Add(int64(len(dropped)))
}

func logPackageMatches(p pkg.Package, matches []match.Match) {
	if len(matches) == 0 {
		return
	}

	log.WithFields("package", displayPackage(p)).Debugf("found %d vulnerabilities", len(matches))
	for idx, m := range matches {
		arm := selectArm(idx, len(matches))
		log.WithFields("vuln", m.Vulnerability.ID, "namespace", m.Vulnerability.Namespace).Tracef("  %s", arm)
	}
}

func selectArm(idx, total int) string {
	if idx == total-1 {
		return leaf
	}
	return branch
}

func logExplicitDroppedPackageMatches(p pkg.Package, ignored []match.IgnoredMatch) {
	if len(ignored) == 0 {
		return
	}

	log.WithFields("package", displayPackage(p)).Debugf("dropped %d vulnerability matches due to hard-coded correction", len(ignored))
	for idx, i := range ignored {
		arm := selectArm(idx, len(ignored))

		log.WithFields("vuln", i.Match.Vulnerability.ID, "rules", len(i.AppliedIgnoreRules)).Tracef("  %s", arm)
	}
}

func logIgnoredMatches(ignored []match.IgnoredMatch) {
	if len(ignored) == 0 {
		return
	}

	log.Infof("ignored %d vulnerability matches", len(ignored))
	for idx, i := range ignored {
		arm := selectArm(idx, len(ignored))
		rule := ""
		if len(i.AppliedIgnoreRules) > 0 {
			rule = i.AppliedIgnoreRules[0].Reason
			if rule == "" {
				rule = i.AppliedIgnoreRules[0].Vulnerability
			}
		}
		vulnerability.LogDropped(i.Vulnerability.ID, "ignoreRules", rule, i)
		log.WithFields("vuln", i.Match.Vulnerability.ID, "rules", len(i.AppliedIgnoreRules), "package", displayPackage(i.Package)).Debugf("  %s", arm)
	}
}

type monitorWriter struct {
	PackagesProcessed *progress.Manual
	MatchesDiscovered *progress.Manual
	Fixed             *progress.Manual
	Ignored           *progress.Manual
	Dropped           *progress.Manual
	BySeverity        map[vulnerability.Severity]*progress.Manual
}

func newMonitor(pkgCount int) (monitorWriter, monitor.Matching) {
	manualBySev := make(map[vulnerability.Severity]*progress.Manual)
	for _, severity := range vulnerability.AllSeverities() {
		manualBySev[severity] = progress.NewManual(-1)
	}
	manualBySev[vulnerability.UnknownSeverity] = progress.NewManual(-1)

	m := monitorWriter{
		PackagesProcessed: progress.NewManual(int64(pkgCount)),
		MatchesDiscovered: progress.NewManual(-1),
		Fixed:             progress.NewManual(-1),
		Ignored:           progress.NewManual(-1),
		Dropped:           progress.NewManual(-1),
		BySeverity:        manualBySev,
	}

	monitorableBySev := make(map[vulnerability.Severity]progress.Monitorable)
	for sev, manual := range manualBySev {
		monitorableBySev[sev] = manual
	}

	return m, monitor.Matching{
		PackagesProcessed: m.PackagesProcessed,
		MatchesDiscovered: m.MatchesDiscovered,
		Fixed:             m.Fixed,
		Ignored:           m.Ignored,
		Dropped:           m.Dropped,
		BySeverity:        monitorableBySev,
	}
}

func (m *monitorWriter) SetCompleted() {
	m.PackagesProcessed.SetCompleted()
	m.MatchesDiscovered.SetCompleted()
	m.Fixed.SetCompleted()
	m.Ignored.SetCompleted()
	m.Dropped.SetCompleted()
	for _, v := range m.BySeverity {
		v.SetCompleted()
	}
}

func trackMatcher(pkgCount int) *monitorWriter {
	writer, reader := newMonitor(pkgCount)

	bus.Publish(partybus.Event{
		Type:  event.VulnerabilityScanningStarted,
		Value: reader,
	})

	return &writer
}

// eolTracker handles checking and caching EOL status for distros
type eolTracker struct {
	checker vulnerability.EOLChecker
	cache   map[string]eolCacheEntry
}

type eolCacheEntry struct {
	isEOL   bool
	eolDate *time.Time
}

func newEOLTracker(enabled bool, provider vulnerability.Provider) *eolTracker {
	if !enabled {
		return &eolTracker{}
	}
	checker, ok := provider.(vulnerability.EOLChecker)
	if !ok {
		return &eolTracker{}
	}
	return &eolTracker{
		checker: checker,
		cache:   make(map[string]eolCacheEntry),
	}
}

// checkAndTrack checks if the package is from an EOL distro and returns true if so.
// Results are cached per distro.
func (t *eolTracker) checkAndTrack(p pkg.Package) bool {
	if t.checker == nil || p.Distro == nil {
		return false
	}

	distroKey := p.Distro.String()
	entry, checked := t.cache[distroKey]
	if !checked {
		eolDate, _, err := t.checker.GetOperatingSystemEOL(p.Distro)
		if err != nil {
			log.WithFields("distro", distroKey, "error", err).Debug("error checking EOL status")
		}
		entry = eolCacheEntry{
			isEOL:   eolDate != nil && eolDate.Before(time.Now()),
			eolDate: eolDate,
		}
		t.cache[distroKey] = entry
	}

	if entry.isEOL {
		log.WithFields("package", displayPackage(p), "distro", distroKey, "eol_date", entry.eolDate).Debug("package from EOL distro")
		return true
	}
	return false
}
