package agent

import (
	"fmt"
	"net/url"
	"regexp"
	"strconv"
	"time"

	report "gitlab.com/gitlab-org/security-products/analyzers/report/v3"
)

var (
	// This regex captures 2 groups,
	// First group being characters before: ` (`
	// Second group being characters between `(` and `)`
	// Example input: "nginx:1.16 (debian 10.3)"
	// Expected output: 1st_group="nginx:1.16" 2nd_group="debian 10.3"
	convertTargetRegexp = regexp.MustCompile(`^(.+)\s\((.+)\)$`)
)

// Type referenced from Trivy https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/k8s/report/report.go?ref_type=tags#L51
type ConsolidatedReport struct {
	Findings []Resource `json:"Findings"`
}

// Type referenced from Trivy https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/k8s/report/report.go#L58
type Resource struct {
	Namespace string   `json:"Namespace"`
	Kind      string   `json:"Kind"`
	Name      string   `json:"Name"`
	Results   []Result `json:"Results"`
}

// Type referenced from Trivy https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/types/report.go#L71
type Result struct {
	Target          string                  `json:"Target"`
	Class           string                  `json:"Class"`
	Type            string                  `json:"Type"`
	Vulnerabilities []DetectedVulnerability `json:"Vulnerabilities"`
}

// Type referenced from Trivy https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/types/vulnerability.go#L9
type DetectedVulnerability struct {
	VulnerabilityID  string `json:"VulnerabilityID"`
	PkgName          string `json:"PkgName"`
	InstalledVersion string `json:"InstalledVersion"`
	FixedVersion     string `json:"FixedVersion"`
	PrimaryURL       string `json:"PrimaryURL"`

	// Embed vulnerability details
	Vulnerability
}

// Type referenced from Trivy-db https://gitlab.com/gitlab-org/security-products/dependencies/trivy-db/-/blob/4bcdf1c414d0/pkg/types/types.go#L132 referenced by Trivy v0.38.3
type Vulnerability struct {
	Title            string     `json:"Title"`
	Description      string     `json:"Description"`
	Severity         string     `json:"Severity"` // Selected from VendorSeverity, depending on a scan target
	References       []string   `json:"References"`
	PublishedDate    *time.Time `json:"PublishedDate"`    // Take from NVD
	LastModifiedDate *time.Time `json:"LastModifiedDate"` // Take from NVD
}

var TrivyScanner = report.ScannerDetails{
	ID:   "starboard_trivy",
	Name: "Trivy (via Starboard Operator)",
	Vendor: report.Vendor{
		Name: "GitLab",
	},
}

// Convert turns a Trivy k8s vulnerability report into a slice of payloads which
// can be sent to the internal vulnerability API
func Convert(findings []Resource, agentID int64) ([]*Payload, error) {
	payloads := make([]*Payload, 0)
	for _, finding := range findings {
		imageName, operatingSystem := findImageName(finding)
		kubernetesResource := convertKubernetesResource(finding, agentID)
		results := finding.Results
		for _, result := range results {
			vulns := result.Vulnerabilities
			for _, vuln := range vulns {
				payload := convert(vuln)
				payload.Vulnerability.Location = convertLocation(imageName, operatingSystem, kubernetesResource, vuln)
				payload.Scanner.Version = TrivyScannerVersion
				payloads = append(payloads, payload)
			}
		}
	}

	return payloads, nil
}

type Payload struct {
	Vulnerability *report.Vulnerability `json:"vulnerability"`
	Scanner       report.ScannerDetails `json:"scanner"`
}

func convert(vuln DetectedVulnerability) *Payload {
	return &Payload{
		Vulnerability: convertVulnerability(vuln),
		Scanner:       TrivyScanner,
	}
}

func convertVulnerability(vuln DetectedVulnerability) *report.Vulnerability {
	return &report.Vulnerability{
		Name:        vuln.VulnerabilityID,
		Message:     fmt.Sprintf("%s in %s", vuln.VulnerabilityID, vuln.PkgName),
		Description: vuln.Description,
		Solution:    fmt.Sprintf("Upgrade %s from %s to %s", vuln.PkgName, vuln.InstalledVersion, vuln.FixedVersion),
		Severity:    convertSeverity(vuln.Severity),
		Confidence:  report.ConfidenceLevelUnknown,
		Identifiers: convertIdentifiers(vuln),
		Links:       convertLinks(vuln),
	}
}

// Adapted from severityNames in Trivy-db https://gitlab.com/gitlab-org/security-products/dependencies/trivy-db/-/blob/2bd1364579ec652f8f595c4a61595fd9575e8496/pkg/types/types.go#L35
const (
	SeverityCritical = "CRITICAL"
	SeverityHigh     = "HIGH"
	SeverityMedium   = "MEDIUM"
	SeverityLow      = "LOW"

	SeverityNone    = "NONE" // Kept for legacy reasons since starboard contains this severity level
	SeverityUnknown = "UNKNOWN"
)

var severityMapping = map[string]report.SeverityLevel{
	SeverityCritical: report.SeverityLevelCritical,
	SeverityHigh:     report.SeverityLevelHigh,
	SeverityMedium:   report.SeverityLevelMedium,
	SeverityLow:      report.SeverityLevelLow,
	SeverityNone:     report.SeverityLevelInfo,
	SeverityUnknown:  report.SeverityLevelUnknown,
}

func convertSeverity(severity string) report.SeverityLevel {
	sev, ok := severityMapping[severity]
	if !ok {
		return report.SeverityLevelUnknown
	}
	return sev
}

func convertLocation(image string, operatingSystem string, kubernetesResource *report.KubernetesResource, vuln DetectedVulnerability) report.Location {
	return report.Location{
		Dependency: &report.Dependency{
			Package: report.Package{Name: vuln.PkgName},
			Version: vuln.InstalledVersion,
		},
		KubernetesResource: kubernetesResource,
		Image:              image,
		OperatingSystem:    operatingSystem,
	}
}

// Location is used to fingerprint(uniquely identify) the finding in gitlab. The fields used for fingerprinting are: agentID, k8sresource.namespace, k8sresource.kind, k8sresource.name, k8sresource.container, PkgName
// As defined here: https://gitlab.com/gitlab-org/gitlab/-/blob/f50075762cf33d3841b88bb191770776b07ede77/ee/app/services/vulnerabilities/starboard_vulnerability_create_service.rb#L62
// WARNING! Be extra careful when changing these fields as it could cause new findings to be flagged to the user when they might have been previously addressed.
func convertKubernetesResource(finding Resource, agentID int64) *report.KubernetesResource {
	return &report.KubernetesResource{
		Namespace:     finding.Namespace,
		Name:          finding.Name,
		Kind:          finding.Kind,
		AgentID:       strconv.FormatInt(agentID, 10),
		ContainerName: "", //NOTE In Trivy k8s, the ContainerName is not provided. https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/k8s/report/report.go#L58-L69.
		// Leaving ContainerName as an empty string as such.
		// This does not affect the fingerprint as the field referenced in gitlab is `container` while the one defined in KubernetesResource is `container_name`.
	}
}

func convertIdentifiers(vuln DetectedVulnerability) []report.Identifier {
	id := vuln.VulnerabilityID
	return []report.Identifier{
		{
			Type:  report.IdentifierTypeCVE,
			Name:  id,
			Value: id,
			URL:   fmt.Sprintf("https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s", url.QueryEscape(id)),
		},
	}
}

func convertLinks(vuln DetectedVulnerability) []report.Link {
	var links []report.Link // nolint:prealloc
	if vuln.PrimaryURL != "" {
		links = append(links, report.Link{URL: vuln.PrimaryURL})
	}

	for _, r := range vuln.References {
		links = append(links, report.Link{URL: r})
	}
	return links
}

// findImageName identifies the image associated with the finding.
// When transmitting report to Gitlab the image name is required for users to identify the source of the vulnerability.
// Trivy k8s scans for both OS and language vulnerabilities.
// For os-pkgs, result.Target is the image name that is being scanned.
// For lang-pkgs(language packages), result.Target is a directory eg `usr/local/bin/trivy` which is not useful to users in identifying the image that contains the vulnerability.
// This function serves to provide the image name for language package vulnerabilities.
func findImageName(finding Resource) (string, string) {
	for _, result := range finding.Results {
		if result.Class == "os-pkgs" {
			imageName, operatingSystem := convertTarget(result.Target)
			return imageName, operatingSystem
		}
	}
	return "", ""
}

// convertTarget converts the target into imageName and OS strings
// Target example "nginx:1.16 (debian 10.3)" would output imageName=`nginx:1.16` OS=`debian 10.3`
// Target is defined here:
// https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/scanner/local/scan.go#L281
func convertTarget(target string) (string, string) {
	match := convertTargetRegexp.FindStringSubmatch(target)

	if len(match) == 3 {
		return match[1], match[2]
	}
	return "", ""
}
