// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package vuln

import (
	"encoding/json"
	"fmt"

	v1 "github.com/goharbor/harbor/src/pkg/scan/rest/v1"
)

// Report model for vulnerability scan
type Report struct {
	// Time of generating this report
	GeneratedAt string `json:"generated_at"`
	// Scanner of generating this report
	Scanner *v1.Scanner `json:"scanner"`
	// A standard scale for measuring the severity of a vulnerability.
	Severity Severity `json:"severity"`
	// Vulnerability list
	Vulnerabilities []*VulnerabilityItem `json:"vulnerabilities"`

	vulnerabilityItemList *VulnerabilityItemList

	// SBOM sbom content
	SBOM map[string]interface{} `json:"sbom,omitempty"`
}

// GetVulnerabilityItemList returns VulnerabilityItemList from the Vulnerabilities of report
func (report *Report) GetVulnerabilityItemList() *VulnerabilityItemList {
	l := report.vulnerabilityItemList
	if l == nil {
		l = &VulnerabilityItemList{}
		l.Add(report.Vulnerabilities...)

		report.vulnerabilityItemList = l
	}

	return l
}

// MarshalJSON custom function to dump nil slice of Vulnerabilities as empty slice
// See https://github.com/goharbor/harbor/issues/11131 to get more details
func (report *Report) MarshalJSON() ([]byte, error) {
	type Alias Report

	aux := &struct {
		*Alias
	}{
		Alias: (*Alias)(report),
	}

	if aux.Vulnerabilities == nil {
		aux.Vulnerabilities = []*VulnerabilityItem{}
	}

	return json.Marshal(aux)
}

// Merge ...
func (report *Report) Merge(another *Report) *Report {
	scanner := report.Scanner
	generatedAt := report.GeneratedAt
	if another.GeneratedAt > report.GeneratedAt {
		generatedAt = another.GeneratedAt

		// choose the scanner from the newer summary
		// because the generatedAt of the report is from the newer report
		scanner = another.Scanner
	}

	l := report.GetVulnerabilityItemList()
	l.Add(another.Vulnerabilities...)

	r := &Report{
		GeneratedAt:           generatedAt,
		Scanner:               scanner,
		Severity:              mergeSeverity(report.Severity, another.Severity),
		Vulnerabilities:       l.Items(),
		vulnerabilityItemList: l,
	}

	return r
}

// WithArtifactDigest set artifact digest for the report
func (report *Report) WithArtifactDigest(artifactDigest string) {
	for _, vul := range report.Vulnerabilities {
		vul.ArtifactDigests = []string{artifactDigest}
	}
}

// NewVulnerabilityItemList returns VulnerabilityItemList from lists
func NewVulnerabilityItemList(lists ...*VulnerabilityItemList) *VulnerabilityItemList {
	var availableLists []*VulnerabilityItemList
	for _, li := range lists {
		if li != nil {
			availableLists = append(availableLists, li)
		}
	}

	if len(availableLists) == 0 {
		return nil
	}

	l := &VulnerabilityItemList{}
	for _, li := range availableLists {
		l.Add(li.Items()...)
	}

	return l
}

// VulnerabilityItemList the list can skip the VulnerabilityItem exists in the list when adding
type VulnerabilityItemList struct {
	items   []*VulnerabilityItem
	indexed map[string]*VulnerabilityItem
}

// Items returns the vulnerabilities in the l
func (l *VulnerabilityItemList) Items() []*VulnerabilityItem {
	return l.items
}

// Add add item to the list when the item not exists in list
func (l *VulnerabilityItemList) Add(items ...*VulnerabilityItem) {
	if l.indexed == nil {
		l.indexed = map[string]*VulnerabilityItem{}
	}

	for _, item := range items {
		key := item.Key()
		if v, ok := l.indexed[key]; ok {
			v.ArtifactDigests = append(v.ArtifactDigests, item.ArtifactDigests...)
		} else {
			l.items = append(l.items, item)
			l.indexed[key] = item
		}
	}
}

// GetItem returns VulnerabilityItem by key
func (l *VulnerabilityItemList) GetItem(key string) (*VulnerabilityItem, bool) {
	item, ok := l.indexed[key]

	return item, ok
}

// GetSeveritySummary returns the severity and summary of l
func (l *VulnerabilityItemList) GetSeveritySummary() (Severity, *VulnerabilitySummary) {
	if l == nil {
		return Severity(""), nil
	}

	sum := &VulnerabilitySummary{
		Total:   len(l.Items()),
		Summary: make(SeveritySummary),
	}

	severity := None
	for _, v := range l.Items() {
		if num, ok := sum.Summary[v.Severity]; ok {
			sum.Summary[v.Severity] = num + 1
		} else {
			sum.Summary[v.Severity] = 1
		}

		// Update the overall severity if necessary
		if v.Severity.Code() > severity.Code() {
			severity = v.Severity
		}
		// If the CVE item has a fixable version
		if len(v.FixVersion) > 0 {
			sum.Fixable++
		}
	}

	return severity, sum
}

// VulnerabilityItem represents one found vulnerability
type VulnerabilityItem struct {
	// The unique identifier of the vulnerability.
	// e.g: CVE-2017-8283
	ID string `json:"id"`
	// An operating system or software dependency package containing the vulnerability.
	// e.g: dpkg
	Package string `json:"package"`
	// The version of the package containing the vulnerability.
	// e.g: 1.17.27
	Version string `json:"version"`
	// The version of the package containing the fix if available.
	// e.g: 1.18.0
	FixVersion string `json:"fix_version"`
	// A standard scale for measuring the severity of a vulnerability.
	Severity Severity `json:"severity"`
	// example: dpkg-source in dpkg 1.3.0 through 1.18.23 is able to use a non-GNU patch program
	// and does not offer a protection mechanism for blank-indented diff hunks, which allows remote
	// attackers to conduct directory traversal attacks via a crafted Debian source package, as
	// demonstrated by using of dpkg-source on NetBSD.
	Description string `json:"description"`
	// The list of link to the upstream database with the full description of the vulnerability.
	// Format: URI
	// e.g: List [ "https://security-tracker.debian.org/tracker/CVE-2017-8283" ]
	Links []string `json:"links"`
	// The artifact digests which the vulnerability belonged
	// e.g: sha256@ee1d00c5250b5a886b09be2d5f9506add35dfb557f1ef37a7e4b8f0138f32956
	ArtifactDigests []string `json:"artifact_digests"`
	// The CVSS3 and CVSS2 based scores and attack vector for the vulnerability item
	CVSSDetails CVSS `json:"preferred_cvss"`
	// A separated list of CWE Ids associated with this vulnerability
	// e.g. CWE-465,CWE-124
	CWEIds []string `json:"cwe_ids"`
	// A collection of vendor specific attributes for the vulnerability item
	// with each attribute represented as a key-value pair.
	VendorAttributes map[string]interface{} `json:"vendor_attributes"`
}

// Key returns the uniq key for the item
func (item *VulnerabilityItem) Key() string {
	return fmt.Sprintf("%s-%s-%s", item.ID, item.Package, item.Version)
}

// CVSS holds the score and attack vector for the vulnerability based on the CVSS3 and CVSS2 standards
type CVSS struct {
	// The CVSS-3 score for the vulnerability
	// e.g. 2.5
	ScoreV3 *float64 `json:"score_v3"`
	// The CVSS-3 score for the vulnerability
	// e.g. 2.5
	ScoreV2 *float64 `json:"score_v2"`
	// The CVSS-3 attack vector.
	// e.g. CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
	VectorV3 string `json:"vector_v3"`
	// The CVSS-3 attack vector.
	// e.g. AV:L/AC:M/Au:N/C:P/I:N/A:N
	VectorV2 string `json:"vector_v2"`
}
