package grype

import (
	"fmt"
	"os"

	"github.com/anchore/grype/grype"
	"github.com/anchore/grype/grype/db/v6/distribution"
	"github.com/anchore/grype/grype/db/v6/installation"
	"github.com/anchore/grype/grype/match"
	"github.com/anchore/grype/grype/matcher"
	"github.com/anchore/grype/grype/pkg"
	"github.com/anchore/grype/grype/vulnerability"
	"github.com/anchore/stereoscope/pkg/image"
	"github.com/anchore/syft/syft"
	"github.com/ckotzbauer/libk8soci/pkg/oci"
	"github.com/ckotzbauer/vulnerability-operator/internal/vuln"
	"github.com/ckotzbauer/vulnerability-operator/internal/vuln/filter"
	"github.com/ckotzbauer/vulnerability-operator/internal/vuln/kubernetes"
	"github.com/ckotzbauer/vulnerability-operator/internal/vuln/source"
	"github.com/sirupsen/logrus"
	"gopkg.in/yaml.v3"
)

type Grype struct {
	provider       vulnerability.Provider
	kubeClient     *kubernetes.KubeClient
	config         grypeConfig
	relatedEntries map[string]string
	onlyFixed      bool
	minSeverity    string
	filterEngine   *filter.FilterEngine
	testContainer  *kubernetes.ContainerInfo
}

type grypeConfig struct {
	Ignore []match.IgnoreRule `yaml:"ignore"`
}

func New(grypeConfigFile, filterConfigFile, minSeverity string, onlyFixed, withoutK8s bool) (Grype, error) {
	grypeCfg, err := loadConfig(grypeConfigFile, grypeConfig{})
	if err != nil {
		return Grype{}, err
	}

	distConfig := distribution.Config{
		LatestURL: "https://grype.anchore.io/databases",
	}

	installConfig := installation.Config{
		DBRootDir: "/tmp/grype/db",
	}

	logrus.Debug("Load vulnerability database")
	provider, dbStatus, err := grype.LoadVulnerabilityDB(distConfig, installConfig, true)
	if err = validateDBLoad(err, dbStatus); err != nil {
		logrus.Error(err)
		return Grype{}, err
	}

	var client *kubernetes.KubeClient

	if !withoutK8s {
		client = kubernetes.NewClient()
	}

	filterConfig, err := loadConfig(filterConfigFile, filter.FilterConfig{})
	if err != nil {
		return Grype{}, err
	}

	return Grype{
		provider:       provider,
		kubeClient:     client,
		config:         *grypeCfg,
		minSeverity:    minSeverity,
		onlyFixed:      onlyFixed,
		relatedEntries: map[string]string{},
		filterEngine:   filter.NewFilterEngine(filterConfig),
	}, nil
}

func (s *Grype) WithTestContainer(c *kubernetes.ContainerInfo) *Grype {
	s.testContainer = c
	return s
}

func (s *Grype) ScanItem(item source.ScanItem) (vuln.ScanResult, error) {
	item, err := preprocessScan(item)
	if err != nil {
		return vuln.ScanResult{}, err
	}

	registryOptions := &image.RegistryOptions{}
	if img, ok := item.(source.Image); ok {
		registryOptions.Credentials = oci.ConvertSecrets(img.RegistryImage, map[string]string{})
	}

	packages, context, _, err := pkg.Provide(item.ScanInput(), pkg.ProviderConfig{
		SyftProviderConfig: pkg.SyftProviderConfig{
			SBOMOptions:     syft.DefaultCreateSBOMConfig(),
			RegistryOptions: registryOptions,
		},
	})
	if err != nil {
		logrus.WithError(err).Error("Grype scan failed")
		return vuln.ScanResult{}, err
	}

	err = item.Cleanup()
	if err != nil {
		logrus.WithError(err).Warn("File could not be deleted")
	}

	if err != nil {
		logrus.WithError(fmt.Errorf("failed to catalog: %w", err)).Error()
		return vuln.ScanResult{}, err
	}

	matchers := matcher.NewDefaultMatchers(matcher.Config{})
	runner := grype.VulnerabilityMatcher{
		VulnerabilityProvider: s.provider,
		Matchers:              matchers,
		NormalizeByCVE:        false,
		IgnoreRules:           s.config.Ignore,
	}

	remainingMatches, _, err := runner.FindMatches(packages, pkg.Context{Distro: context.Distro})
	if err != nil {
		logrus.WithError(err).Error("Failed to find matches")
		return vuln.ScanResult{}, err
	}

	vulns := s.buildVulnerabilities(*remainingMatches, item.ImageId())
	return s.filterVulnerabilities(vulns), nil
}

func (s *Grype) Close() {
	err := s.provider.Close()
	if err != nil {
		logrus.WithError(err).Error("Closing provider failed")
	}
}

func preprocessScan(item source.ScanItem) (source.ScanItem, error) {
	if sbom, ok := item.(source.Sbom); ok {
		file, err := os.CreateTemp("", "sbom*")
		if err != nil {
			logrus.WithError(err).Error("Error creating temp-file")
			return nil, err
		}

		sbom.File = file.Name()
		_, err = file.WriteString(sbom.Sbom)
		if err != nil {
			logrus.WithError(err).Error("SBOM could not be saved")
			return nil, err
		}

		return sbom, nil
	}

	return item, nil
}

func validateDBLoad(loadErr error, status *vulnerability.ProviderStatus) error {
	if loadErr != nil {
		return fmt.Errorf("failed to load vulnerability db: %w", loadErr)
	}
	if status == nil {
		return fmt.Errorf("unable to determine DB status")
	}
	if status.Error != nil {
		return fmt.Errorf("db could not be loaded: %w", status.Error)
	}
	return nil
}

func (s *Grype) buildVulnerabilities(matches match.Matches, imageID string) []vuln.Vulnerability {
	vulnerabilities := make([]vuln.Vulnerability, 0)

	for m := range matches.Enumerate() {
		fixedIn := []string{}
		if m.Vulnerability.Fix.State == vulnerability.FixStateFixed {
			fixedIn = m.Vulnerability.Fix.Versions
		}

		if m.Vulnerability.RelatedVulnerabilities != nil {
			for _, ref := range m.Vulnerability.RelatedVulnerabilities {
				s.relatedEntries[fmt.Sprintf("%s:%s", m.Vulnerability.Namespace, m.Vulnerability.ID)] =
					fmt.Sprintf("%s:%s", ref.Namespace, ref.ID)
			}
		}

		v := vuln.Vulnerability{
			ID:         m.Vulnerability.ID,
			Namespace:  m.Vulnerability.Namespace,
			Severity:   m.Vulnerability.Metadata.Severity,
			Type:       string(m.Package.Type),
			Package:    m.Package.Name,
			Installed:  m.Package.Version,
			FixedIn:    fixedIn,
			FixState:   string(m.Vulnerability.Fix.State),
			URLs:       m.Vulnerability.Metadata.URLs,
			ImageID:    imageID,
			Containers: []kubernetes.ContainerInfo{},
		}

		if s.kubeClient != nil {
			infos, _ := s.kubeClient.GetContainersWithImage(imageID)
			v.Containers = infos
		} else if s.testContainer != nil {
			v.Containers = append(v.Containers, *s.testContainer)
		}

		vulnerabilities = append(vulnerabilities, v)
	}

	return vulnerabilities
}

func (s *Grype) filterVulnerabilities(allVulns []vuln.Vulnerability) vuln.ScanResult {
	minSeverity := vulnerability.ParseSeverity(s.minSeverity)

	vulns := make([]vuln.Vulnerability, 0)

	for _, v := range allVulns {
		if s.hasRelated(v, allVulns) {
			continue
		}

		sev := vulnerability.ParseSeverity(v.Severity)
		if sev >= minSeverity && v.Installed != "" {
			if s.onlyFixed {
				if v.FixState == string(vulnerability.FixStateFixed) {
					vulns = append(vulns, v)
				}
			} else {
				vulns = append(vulns, v)
			}
		}
	}

	foundVulns, auditedVulns := s.filterEngine.FilterVulnerabilities(vulns)
	return vuln.ScanResult{FoundVulnerabilities: foundVulns, AuditedVulnerabilities: auditedVulns}
}

func (s *Grype) hasRelated(v vuln.Vulnerability, all []vuln.Vulnerability) bool {
	for original, related := range s.relatedEntries {
		if original == fmt.Sprintf("%s:%s", v.Namespace, v.ID) {
			for _, a := range all {
				if fmt.Sprintf("%s:%s", a.Namespace, a.ID) == related {
					return true
				}
			}

			return false
		}
	}

	return false
}

func loadConfig[V interface{}](configFile string, cfg V) (*V, error) {
	if configFile != "" {
		_, err := os.Stat(configFile)
		if err == nil {
			/* #nosec */
			data, err := os.ReadFile(configFile)
			if err != nil {
				logrus.WithError(err).Errorf("Count not load config-file at %s", configFile)
				return nil, err
			}

			err = yaml.Unmarshal(data, &cfg)
			if err != nil {
				logrus.WithError(err).Error("Failed to unmarshal the config")
				return nil, err
			}
		}

		if err != nil {
			logrus.Error(err)
			return nil, err
		}
	}

	return &cfg, nil
}
