package vulnerability

import (
	"bufio"
	"fmt"
	"os"
	"strings"

	"github.com/knqyf263/trivy/pkg/utils"

	"sort"

	"github.com/knqyf263/trivy/pkg/log"
)

const (
	trivyIgnore = ".trivyignore"
)

var (
	sources = []string{Nvd, RedHat, Debian, DebianOVAL, Alpine,
		RubySec, RustSec, PhpSecurityAdvisories, NodejsSecurityWg, PythonSafetyDB}
)

func FillAndFilter(vulns []DetectedVulnerability, severities []Severity, ignoreUnfixed bool) []DetectedVulnerability {
	ignoredIDs := getIgnoredIDs()
	var vulnerabilities []DetectedVulnerability
	for _, vuln := range vulns {
		sev, title, description, references := getDetail(vuln.VulnerabilityID)

		// Filter vulnerabilities by severity
		for _, s := range severities {
			if s == sev {
				vuln.Severity = fmt.Sprint(sev)
				vuln.Title = title
				vuln.Description = description
				vuln.References = references

				// Ignore unfixed vulnerabilities
				if ignoreUnfixed && vuln.FixedVersion == "" {
					continue
				} else if utils.StringInSlice(vuln.VulnerabilityID, ignoredIDs) {
					continue
				}
				vulnerabilities = append(vulnerabilities, vuln)
				break
			}
		}
	}
	sort.Slice(vulnerabilities, func(i, j int) bool {
		if vulnerabilities[i].PkgName != vulnerabilities[j].PkgName {
			return vulnerabilities[i].PkgName < vulnerabilities[j].PkgName
		}
		return CompareSeverityString(vulnerabilities[j].Severity, vulnerabilities[i].Severity)
	})
	return vulnerabilities
}

func getIgnoredIDs() []string {
	f, err := os.Open(trivyIgnore)
	if err != nil {
		// trivy must work even if no .trivyignore exist
		return nil
	}

	var ignoredIDs []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "#") || line == "" {
			continue
		}
		ignoredIDs = append(ignoredIDs, line)
	}
	return ignoredIDs
}

func getDetail(vulnID string) (Severity, string, string, []string) {
	details, err := Get(vulnID)
	if err != nil {
		log.Logger.Debug(err)
		return SeverityUnknown, "", "", nil
	} else if len(details) == 0 {
		return SeverityUnknown, "", "", nil
	}
	return getSeverity(details), getTitle(details), getDescription(details), getReferences(details)
}

func getSeverity(details map[string]Vulnerability) Severity {
	for _, source := range sources {
		switch d, ok := details[source]; {
		case !ok:
			continue
		case d.CvssScore > 0:
			return scoreToSeverity(d.CvssScore)
		case d.CvssScoreV3 > 0:
			return scoreToSeverity(d.CvssScoreV3)
		case d.Severity != 0:
			return d.Severity
		case d.SeverityV3 != 0:
			return d.SeverityV3
		}
	}
	return SeverityUnknown
}

func getTitle(details map[string]Vulnerability) string {
	for _, source := range sources {
		d, ok := details[source]
		if !ok {
			continue
		}
		if d.Title != "" {
			return d.Title
		}
	}
	return ""
}

func getDescription(details map[string]Vulnerability) string {
	for _, source := range sources {
		d, ok := details[source]
		if !ok {
			continue
		}
		if d.Description != "" {
			return d.Description
		}
	}
	return ""
}

func getReferences(details map[string]Vulnerability) []string {
	references := map[string]struct{}{}
	for _, source := range sources {
		d, ok := details[source]
		if !ok {
			continue
		}
		for _, ref := range d.References {
			references[ref] = struct{}{}
		}
	}
	var refs []string
	for ref := range references {
		refs = append(refs, ref)
	}
	return refs
}

func scoreToSeverity(score float64) Severity {
	switch {
	case score >= 9.0:
		return SeverityCritical
	case score >= 7.0:
		return SeverityHigh
	case score >= 4.0:
		return SeverityMedium
	case score > 0.0:
		return SeverityLow
	default:
		return SeverityUnknown
	}
}
