package osvscanner

import (
	"path/filepath"
	"slices"
	"sort"
	"strings"

	"github.com/google/osv-scalibr/extractor/filesystem/sbom/cdx"
	cdxmeta "github.com/google/osv-scalibr/extractor/filesystem/sbom/cdx/metadata"
	"github.com/google/osv-scalibr/inventory/vex"
	"github.com/google/osv-scanner/v2/internal/cmdlogger"
	"github.com/google/osv-scanner/v2/internal/grouper"
	"github.com/google/osv-scanner/v2/internal/imodels"
	"github.com/google/osv-scanner/v2/internal/imodels/results"
	"github.com/google/osv-scanner/v2/internal/output"
	"github.com/google/osv-scanner/v2/internal/sourceanalysis"
	"github.com/google/osv-scanner/v2/internal/spdx"
	"github.com/google/osv-scanner/v2/pkg/models"
	"github.com/google/osv-scanner/v2/pkg/osvscanner/internal/imagehelpers"
	"github.com/ossf/osv-schema/bindings/go/osvconstants"
	"github.com/ossf/osv-schema/bindings/go/osvschema"
)

// buildVulnerabilityResults takes the responses from the OSV API and the deps.dev API
// and converts this into a VulnerabilityResults. As part is this, it groups
// vulnerability information by source location.
// TODO: This function is getting long, we should refactor it
func buildVulnerabilityResults(
	actions ScannerActions,
	scanResults *results.ScanResults,
) models.VulnerabilityResults {
	vulnResults := models.VulnerabilityResults{
		Results:                     []models.PackageSource{},
		ImageMetadata:               imagehelpers.BuildImageMetadata(scanResults),
		ExperimentalGenericFindings: scanResults.Inventory.GenericFindings,
	}

	type packageVulnsGroup struct {
		pvs         []models.PackageVulns
		annotations []*vex.PackageExploitabilitySignal
	}

	groupedBySource := map[models.SourceInfo]*packageVulnsGroup{}

	for i, p := range scanResults.Inventory.Packages {
		includePackage := actions.ShowAllPackages
		var pkg models.PackageVulns

		pkg.Package.Inventory = p

		if imodels.Commit(p) != "" {
			pkg.Package.Commit = imodels.Commit(p)
			pkg.Package.Name = imodels.Name(p)
		}

		pkg.Package.Name = imodels.Name(p)
		pkg.Package.Version = imodels.Version(p)
		pkg.Package.Ecosystem = imodels.Ecosystem(p).String()
		pkg.Package.OSPackageName = imodels.OSPackageName(p)
		pkg.Package.Deprecated = p.Deprecated

		if pkg.Package.Deprecated {
			includePackage = true
		}

		if p.LayerMetadata != nil {
			pkg.Package.ImageOrigin = &models.ImageOriginDetails{
				Index: p.LayerMetadata.Index,
			}
		}
		pkg.DepGroups = imodels.DepGroups(p)
		configToUse := scanResults.ConfigManager.Get(imodels.Location(p))

		haveCheckedIgnore := false
		for _, vuln := range scanResults.Inventory.PackageVulns {
			if vuln.Package == p {
				if !haveCheckedIgnore {
					if configToUse.ShouldIgnorePackageVulnerabilities(p) {
						break
					}

					haveCheckedIgnore = true
				}

				includePackage = true
				pkg.Vulnerabilities = append(pkg.Vulnerabilities, vuln.Vulnerability)
			}
		}

		pkg.Groups = grouper.Group(grouper.ConvertVulnerabilityToIDAliases(pkg.Vulnerabilities))
		for i, group := range pkg.Groups {
			pkg.Groups[i].MaxSeverity = output.MaxSeverity(group, pkg)
		}

		// For Debian-based ecosystems, mark unimportant vulnerabilities within the package.
		// Debian ecosystems may be listed with a version number, such as "Debian:10".
		if strings.HasPrefix(pkg.Package.Ecosystem, string(osvconstants.EcosystemDebian)) ||
			strings.HasPrefix(pkg.Package.Ecosystem, string(osvconstants.EcosystemUbuntu)) {
			setUnimportant(&pkg)
		}

		if actions.CallAnalysisStates["jar"] {
			setUncalled(&pkg)
		}

		if actions.ScanLicensesSummary || len(actions.ScanLicensesAllowlist) > 0 {
			if override, entry := configToUse.ShouldOverridePackageLicense(p); override {
				if entry.License.Ignore {
					cmdlogger.Infof("ignoring license for package %s/%s/%s", pkg.Package.Ecosystem, pkg.Package.Name, pkg.Package.Version)
					p.Licenses = []string{}
				} else {
					cmdlogger.Infof("overriding license for package %s/%s/%s with %s", pkg.Package.Ecosystem, pkg.Package.Name, pkg.Package.Version, strings.Join(entry.License.Override, ","))
					p.Licenses = entry.License.Override
				}
			}
			if len(actions.ScanLicensesAllowlist) > 0 {
				pkg.Licenses = make([]models.License, len(p.Licenses))
				for j, license := range p.Licenses {
					pkg.Licenses[j] = models.License(license)
				}
				for _, license := range pkg.Licenses {
					satisfies, err := spdx.Satisfies(license, actions.ScanLicensesAllowlist)

					if err != nil {
						cmdlogger.Errorf("license %s for package %s/%s/%s is invalid: %s", license, pkg.Package.Ecosystem, pkg.Package.Name, pkg.Package.Version, err)
					}

					if !satisfies {
						pkg.LicenseViolations = append(pkg.LicenseViolations, license)
					}
				}
				if len(pkg.LicenseViolations) > 0 {
					includePackage = true
				}
			}

			if actions.ScanLicensesSummary {
				pkg.Licenses = make([]models.License, len(p.Licenses))
				for j, license := range p.Licenses {
					pkg.Licenses[j] = models.License(license)
				}
			}

			// Make sure licenses are overridden in the scan results.
			scanResults.Inventory.Packages[i] = p
		}
		if includePackage {
			source := models.SourceInfo{
				Path: filepath.ToSlash(imodels.Location(p)),
				Type: imodels.SourceType(p),
			}

			if slices.Contains(p.Plugins, cdx.Name) {
				locations := p.Metadata.(*cdxmeta.Metadata).CDXLocations
				if len(locations) > 0 {
					source.Path = source.Path + ":" + locations[0]
				}
			}

			if groupedBySource[source] == nil {
				groupedBySource[source] = &packageVulnsGroup{}
			}

			groupedBySource[source].pvs = append(groupedBySource[source].pvs, pkg)
			// Overwrite annotations as it should be the same for the same package.
			groupedBySource[source].annotations = p.ExploitabilitySignals
		}
	}

	// TODO(v2): Move source analysis out of here.
	for source, packages := range groupedBySource {
		sourceanalysis.Run(source, packages.pvs, actions.CallAnalysisStates)
		vulnResults.Results = append(vulnResults.Results, models.PackageSource{
			Source:          source,
			ExperimentalPES: packages.annotations,
			Packages:        packages.pvs,
		})
	}

	sort.Slice(vulnResults.Results, func(i, j int) bool {
		if vulnResults.Results[i].Source.Path == vulnResults.Results[j].Source.Path {
			return vulnResults.Results[i].Source.Type < vulnResults.Results[j].Source.Type
		}

		return vulnResults.Results[i].Source.Path < vulnResults.Results[j].Source.Path
	})

	if len(actions.ScanLicensesAllowlist) > 0 || actions.ScanLicensesSummary {
		vulnResults.ExperimentalAnalysisConfig.Licenses.Summary = actions.ScanLicensesSummary
		allowlist := make([]models.License, len(actions.ScanLicensesAllowlist))
		for i, l := range actions.ScanLicensesAllowlist {
			allowlist[i] = models.License(l)
		}
		vulnResults.ExperimentalAnalysisConfig.Licenses.Allowlist = allowlist
	}

	return vulnResults
}

func setUncalled(pv *models.PackageVulns) {
	// Use index to keep reference to original element in slice
	for groupIdx := range pv.Groups {
		for _, vulnID := range pv.Groups[groupIdx].IDs {
			analysis := &pv.Groups[groupIdx].ExperimentalAnalysis
			if *analysis == nil {
				*analysis = make(map[string]models.AnalysisInfo)
			}

			isUncalled := false

			for _, e := range pv.Package.Inventory.ExploitabilitySignals {
				if e.Justification == vex.VulnerableCodeNotInExecutePath {
					isUncalled = true
					break
				}
			}

			(*analysis)[vulnID] = models.AnalysisInfo{
				Called:      !isUncalled,
				Unimportant: (*analysis)[vulnID].Unimportant,
			}
		}
	}
}

// setUnimportant marks vulnerabilities in a PackageVulns as unimportant
// within their respective groups' experimental analysis.
func setUnimportant(pkg *models.PackageVulns) {
	for _, vuln := range pkg.Vulnerabilities {
		if !isUnimportant(vuln) {
			continue
		}
		for i, group := range pkg.Groups {
			if slices.Contains(group.IDs, vuln.GetId()) {
				if group.ExperimentalAnalysis == nil {
					pkg.Groups[i].ExperimentalAnalysis = make(map[string]models.AnalysisInfo)
				}
				// Set unimportant vulns as uncalled
				pkg.Groups[i].ExperimentalAnalysis[vuln.GetId()] = models.AnalysisInfo{
					Unimportant: true,
					// TODO(gongh@): Currently, call analysis is not supported for Linux distribution vulnerabilities.
					// Except explicitly set Called as true to not be counted as uncalled vulnerabilities.
					// Update this behavior when call analysis for Linux distributions is implemented.
					Called: true,
				}

				break
			}
		}
	}
}

// isUnimportant checks if a Debian-based vulnerability is tagged as unimportant
// Debian: https://security-team.debian.org/security_tracker.html#severity-levels
// Ubuntu: https://ubuntu.com/security/cves/about#priority
func isUnimportant(vuln *osvschema.Vulnerability) bool {
	for _, severity := range vuln.GetSeverity() {
		// TODO(gongh@): remove checking empty severity type after all ubuntu records have a valid severity tag.
		if strings.HasPrefix(vuln.GetId(), "UBUNTU-CVE-") &&
			(severity.GetType() == osvschema.Severity_Ubuntu || severity.GetType() == osvschema.Severity_UNSPECIFIED) {
			return severity.GetScore() == "negligible"
		}
	}

	for _, affected := range vuln.GetAffected() {
		if es := affected.GetEcosystemSpecific(); es != nil {
			if fields := es.GetFields(); fields != nil {
				if urgency, ok := fields["urgency"]; ok && urgency != nil {
					if urgency.GetStringValue() == "unimportant" {
						return true
					}
				}
				// TODO (gongh@): Remove this once Ubuntu has fully moved all priority tags into the severity field.
				if priority, ok := fields["ubuntu_priority"]; ok && priority != nil {
					if priority.GetStringValue() == "negligible" {
						return true
					}
				}
			}
		}
	}

	return false
}
