// 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 v1

import (
	"encoding/json"
	"fmt"
	"slices"

	"github.com/goharbor/harbor/src/lib/errors"
)

const (
	supportVulnerability = "support_vulnerability"
	supportSBOM          = "support_sbom"
)

var supportedMimeTypes = []string{
	MimeTypeNativeReport,
	MimeTypeGenericVulnerabilityReport,
	MimeTypeSBOMReport,
}

// Scanner represents metadata of a Scanner Adapter which allow Harbor to lookup a scanner capable of
// scanning a given Artifact stored in its registry and making sure that it can interpret a
// returned result.
type Scanner struct {
	// The name of the scanner.
	Name string `json:"name"`
	// The name of the scanner's provider.
	Vendor string `json:"vendor"`
	// The version of the scanner.
	Version string `json:"version"`
}

// ScannerCapability consists of the set of recognized artifact MIME types and the set of scanner
// report MIME types. For example, a scanner capable of analyzing Docker images and producing
// a vulnerabilities report recognizable by Harbor web console might be represented with the
// following capability:
//   - type: vulnerability
//   - consumes MIME types:
//     -- application/vnd.oci.image.manifest.v1+json
//     -- application/vnd.docker.distribution.manifest.v2+json
//   - produces MIME types
//     -- application/vnd.scanner.adapter.vuln.report.harbor+json; version=1.0
//     -- application/vnd.scanner.adapter.vuln.report.raw
type ScannerCapability struct {
	// The type of the scanner capability, vulnerability or sbom
	Type string `json:"type"`
	// The set of MIME types of the artifacts supported by the scanner to produce the reports
	// specified in the "produces_mime_types". A given mime type should only be present in one
	// capability item.
	ConsumesMimeTypes []string `json:"consumes_mime_types"`
	// The set of MIME types of reports generated by the scanner for the consumes_mime_types of
	// the same capability record.
	ProducesMimeTypes []string `json:"produces_mime_types"`
}

// ScannerProperties is a set of custom properties that can further describe capabilities of a given scanner.
type ScannerProperties map[string]string

// ScannerAdapterMetadata represents metadata of a Scanner Adapter which allows Harbor to lookup
// a scanner capable of scanning a given Artifact stored in its registry and making sure that it
// can interpret a returned result.
type ScannerAdapterMetadata struct {
	Scanner      *Scanner             `json:"scanner"`
	Capabilities []*ScannerCapability `json:"capabilities"`
	Properties   ScannerProperties    `json:"properties"`
}

// Validate validate the metadata
func (md *ScannerAdapterMetadata) Validate() error {
	// Validate the required properties
	if md.Scanner == nil ||
		len(md.Scanner.Name) == 0 ||
		len(md.Scanner.Version) == 0 ||
		len(md.Scanner.Vendor) == 0 {
		return errors.New("invalid scanner in metadata")
	}

	if len(md.Capabilities) == 0 {
		return errors.New("invalid capabilities in metadata")
	}

	for _, ca := range md.Capabilities {
		// v1.MimeTypeDockerArtifact is required now
		found := slices.Contains(ca.ConsumesMimeTypes, MimeTypeDockerArtifact)
		if !found {
			return errors.Errorf("missing %s in consumes_mime_types", MimeTypeDockerArtifact)
		}

		// either of v1.MimeTypeNativeReport OR v1.MimeTypeGenericVulnerabilityReport is required
		found = slices.ContainsFunc(ca.ProducesMimeTypes, func(pm string) bool {
			return isSupportedMimeType(pm)
		})
		if !found {
			return errors.Errorf("missing %s or %s in produces_mime_types", MimeTypeNativeReport, MimeTypeGenericVulnerabilityReport)
		}
	}

	return nil
}

func isSupportedMimeType(mimeType string) bool {
	return slices.Contains(supportedMimeTypes, mimeType)
}

// HasCapability returns true when mine type of the artifact support by the scanner
func (md *ScannerAdapterMetadata) HasCapability(mimeType string) bool {
	return slices.ContainsFunc(md.Capabilities, func(c *ScannerCapability) bool {
		return slices.Contains(c.ConsumesMimeTypes, mimeType)
	})
}

// GetCapability returns capability for the mime type
func (md *ScannerAdapterMetadata) GetCapability(mimeType string) *ScannerCapability {
	for _, capability := range md.Capabilities {
		if slices.Contains(capability.ConsumesMimeTypes, mimeType) {
			return capability
		}
	}

	return nil
}

// ConvertCapability converts the capability to map, used in get scanner API
func (md *ScannerAdapterMetadata) ConvertCapability() map[string]any {
	capabilities := make(map[string]any)
	oldScanner := true
	for _, c := range md.Capabilities {
		if len(c.Type) > 0 {
			oldScanner = false
		}
		if c.Type == ScanTypeVulnerability {
			capabilities[supportVulnerability] = true
		} else if c.Type == ScanTypeSbom {
			capabilities[supportSBOM] = true
		}
	}
	if oldScanner && len(capabilities) == 0 {
		// to compatible with old version scanner, suppose they should always support scan vulnerability when capability is empty
		capabilities[supportVulnerability] = true
		capabilities[supportSBOM] = false
	}
	return capabilities
}

// Artifact represents an artifact stored in Registry.
type Artifact struct {
	// ID of the namespace (project). It will not be sent to scanner adapter.
	NamespaceID int64 `json:"namespace_id,omitempty"`
	// The full name of a Harbor repository containing the artifact, including the namespace.
	// For example, `library/oracle/nosql`.
	Repository string `json:"repository"`
	// The info used to identify the version of the artifact,
	Tag string `json:"tag"`
	// The artifact's digest, consisting of an algorithm and hex portion.
	// For example, `sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b`,
	// represents sha256 based digest.
	Digest string `json:"digest"`
	// The mime type of the scanned artifact
	MimeType string `json:"mime_type"`
	// The size the scanned artifact
	Size int64 `json:"size"`
}

// Registry represents Registry connection settings.
type Registry struct {
	// A base URL of the Docker Registry v2 API exposed by Harbor.
	URL string `json:"url"`
	// An optional value of the HTTP Authorization header sent with each request to the Docker Registry for getting or exchanging token.
	// For example, `Basic: Base64(username:password)`.
	Authorization string `json:"authorization"`
	// Insecure is an indicator of https or http.
	Insecure bool `json:"insecure"`
}

// ScanRequest represents a structure that is sent to a Scanner Adapter to initiate artifact scanning.
// Conducts all the details required to pull the artifact from a Harbor registry.
type ScanRequest struct {
	// Connection settings for the Docker Registry v2 API exposed by Harbor.
	Registry *Registry `json:"registry"`
	// Artifact to be scanned.
	Artifact *Artifact `json:"artifact"`
	// RequestType
	RequestType []*ScanType `json:"enabled_capabilities"`
}

// ScanType represent the type of the scan request
type ScanType struct {
	// Type sets the type of the scan, it could be sbom or vulnerability, default is vulnerability
	Type string `json:"type"`
	// ProducesMimeTypes defines scanreport should be
	ProducesMimeTypes []string `json:"produces_mime_types"`
	// Parameters extra parameters
	Parameters map[string]any `json:"parameters"`
}

// FromJSON parses ScanRequest from json data
func (s *ScanRequest) FromJSON(jsonData string) error {
	if len(jsonData) == 0 {
		return errors.New("empty json data to parse")
	}

	return json.Unmarshal([]byte(jsonData), s)
}

// ToJSON marshals ScanRequest to JSON data
func (s *ScanRequest) ToJSON() (string, error) {
	data, err := json.Marshal(s)
	if err != nil {
		return "", err
	}

	return string(data), nil
}

// Validate ScanRequest
func (s *ScanRequest) Validate() error {
	if s.Registry == nil ||
		len(s.Registry.URL) == 0 {
		return errors.New("scan request: invalid registry")
	}

	if s.Artifact == nil ||
		len(s.Artifact.Digest) == 0 ||
		len(s.Artifact.Repository) == 0 ||
		len(s.Artifact.MimeType) == 0 {
		return errors.New("scan request: invalid artifact")
	}

	return nil
}

// ScanResponse represents the response returned by the scanner adapter after scan request successfully
// submitted.
type ScanResponse struct {
	// e.g: 3fa85f64-5717-4562-b3fc-2c963f66afa6
	ID string `json:"id"`
}

// ErrorResponse contains error message when requests are not correctly handled.
type ErrorResponse struct {
	// Error object
	Err *Error `json:"error"`
}

// Error message
type Error struct {
	// Message of the error
	Message string `json:"message"`
}

// Error for ErrorResponse
func (er *ErrorResponse) Error() string {
	if er.Err != nil {
		return er.Err.Message
	}

	return "nil error"
}

// ReportNotReadyError is an error to indicate the scan report is not ready
type ReportNotReadyError struct {
	// Seconds for next retry with seconds
	RetryAfter int
}

// Error for ReportNotReadyError
func (rnr *ReportNotReadyError) Error() string {
	return fmt.Sprintf("report is not ready yet, retry after %d", rnr.RetryAfter)
}
