package filter

import (
	"github.com/ckotzbauer/vulnerability-operator/internal/vuln"
	"github.com/ckotzbauer/vulnerability-operator/internal/vuln/kubernetes"
)

type FilterEngine struct {
	config *FilterConfig
}

func NewFilterEngine(config *FilterConfig) *FilterEngine {
	return &FilterEngine{config: config}
}

func (f *FilterEngine) FilterVulnerabilities(vulns []vuln.Vulnerability) ([]vuln.Vulnerability, []vuln.Vulnerability) {
	audited := make([]vuln.Vulnerability, 0)
	found := make([]vuln.Vulnerability, 0)

	for _, vuln := range vulns {
		_, notFiltered := matchesAnyFilter(vuln, f.config.Ignore)
		filtered, notFiltered := matchesAnyFilter(notFiltered, f.config.Audit)
		if len(filtered.Containers) > 0 {
			audited = append(audited, filtered)
		}

		if len(notFiltered.Containers) > 0 {
			found = append(found, notFiltered)
		}
	}

	return found, audited
}

func createConcatFilter(v vuln.Vulnerability, filters []VulnerabilityFilter) VulnerabilityFilter {
	concatFilter := VulnerabilityFilter{
		Vulnerability: "",
		Package:       "",
		Context:       make([]FilterContext, 0),
	}

	atLeastOneFilter := false

	for _, filter := range filters {
		if filter.Vulnerability == v.ID || filter.Package == v.Package {
			atLeastOneFilter = true

			// This is an edge case where we have at least one filter with an empty context.
			// It should supercede any more specific filter
			if len(filter.Context) == 0 {
				concatFilter.Context = make([]FilterContext, 0)
				break
			}
			concatFilter.Context = append(concatFilter.Context, filter.Context...)
		}
	}

	if atLeastOneFilter {
		concatFilter.Vulnerability = v.ID
		concatFilter.Package = v.Package
	}

	return concatFilter
}

func matchesAnyFilter(v vuln.Vulnerability, filters []VulnerabilityFilter) (vuln.Vulnerability, vuln.Vulnerability) {
	concatFilter := createConcatFilter(v, filters)
	_, filtered, notFiltered := matches(v, concatFilter)
	return filtered, notFiltered
}

func matches(v vuln.Vulnerability, filter VulnerabilityFilter) (matched bool, appliedVuln vuln.Vulnerability, notAppliedVuln vuln.Vulnerability) {
	appliedContainers := make([]kubernetes.ContainerInfo, 0)
	notAppliedContainers := make([]kubernetes.ContainerInfo, 0)

	defer func() {
		matched = len(appliedContainers) > 0
		appliedVuln = fromVulnerability(v, appliedContainers)
		notAppliedVuln = fromVulnerability(v, notAppliedContainers)
	}()

	// We can just shortcut and return false if this filter does not match.
	if v.ID != filter.Vulnerability && v.Package != filter.Package {
		notAppliedContainers = v.Containers
		return
	}

	if len(filter.Context) > 0 {
		for _, container := range v.Containers {
			name := container.OwnerName
			if container.OwnerKind == "Pod" {
				name = container.PodName
			}

			// if any context matches the container, then the filter matches the container
			applied := false
			for _, ctx := range filter.Context {
				if matchesContainer(v.ImageID, ctx.Image) &&
					matchesContainer(container.OwnerKind, ctx.Kind) &&
					matchesContainer(name, ctx.Name) &&
					matchesContainer(container.Namespace, ctx.Namespace) {
					appliedContainers = append(appliedContainers, container)
					applied = true
					break
				}
			}
			if !applied {
				notAppliedContainers = append(notAppliedContainers, container)
			}
		}
	} else {
		appliedContainers = v.Containers
	}

	return
}

func matchesContainer(value, pattern string) bool {
	if pattern == "" {
		return true
	}

	return vuln.IsMatch(value, pattern)
}

func fromVulnerability(v vuln.Vulnerability, containers []kubernetes.ContainerInfo) vuln.Vulnerability {
	return vuln.Vulnerability{
		ID:         v.ID,
		Namespace:  v.Namespace,
		Severity:   v.Severity,
		Type:       v.Type,
		Package:    v.Package,
		Installed:  v.Installed,
		FixedIn:    v.FixedIn,
		FixState:   v.FixState,
		URLs:       v.URLs,
		ImageID:    v.ImageID,
		Containers: containers,
	}
}
