package grype

import (
	"strings"

	"github.com/anchore/grype/grype/grypeerr"
	"github.com/anchore/grype/grype/match"
	"github.com/anchore/grype/grype/matcher"
	"github.com/anchore/grype/grype/pkg"
	"github.com/anchore/grype/grype/store"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/grype/internal/log"
)

type VulnerabilityMatcher struct {
	Store          store.Store
	Matchers       []matcher.Matcher
	IgnoreRules    []match.IgnoreRule
	FailSeverity   *vulnerability.Severity
	NormalizeByCVE bool
}

func DefaultVulnerabilityMatcher(store store.Store) *VulnerabilityMatcher {
	return &VulnerabilityMatcher{
		Store:    store,
		Matchers: matcher.NewDefaultMatchers(matcher.Config{}),
	}
}

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

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

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

func (m *VulnerabilityMatcher) FindMatches(pkgs []pkg.Package, context pkg.Context) (*match.Matches, []match.IgnoredMatch, error) {
	var ignoredMatches []match.IgnoredMatch
	matches := matcher.FindMatches(m.Store, context.Distro, m.Matchers, pkgs)

	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.
		matches, ignoredMatches = m.applyIgnoreRules(normalizedMatches)
	}

	var err error
	if m.FailSeverity != nil && HasSeverityAtOrAbove(m.Store, *m.FailSeverity, matches) {
		err = grypeerr.ErrAboveSeverityThreshold
	}

	return &matches, ignoredMatches, err
}

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:
		// TODO: trace logging
		return match
	case 1:
		break
	default:
		// TODO: trace logging
		return match
	}

	ref := effectiveCVERecordRefs[0]

	upstreamVulnRecords, err := m.Store.Get(ref.ID, ref.Namespace)
	if err != nil {
		log.Warnf("unable to fetch effective CVE record for id=%q namespace=%q : %v", ref.ID, ref.Namespace, err)
		return match
	}

	switch len(upstreamVulnRecords) {
	case 0:
		// TODO: trace logging
		return match
	case 1:
		break
	default:
		// TODO: trace logging
		return match
	}

	originalRef := vulnerability.Reference{
		ID:        match.Vulnerability.ID,
		Namespace: match.Vulnerability.Namespace,
	}
	match.Vulnerability = upstreamVulnRecords[0]
	match.Vulnerability.RelatedVulnerabilities = append(match.Vulnerability.RelatedVulnerabilities, originalRef)

	return match
}

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

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.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace)
		if err != nil {
			continue
		}

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