// Copyright (c) Facebook, Inc. and its affiliates.
//
// 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 cvss3

import (
	"fmt"
	"strings"
)

const (
	prefix          = "CVSS:"
	partSeparator   = "/"
	metricSeparator = ":"
)

// vector version, int part represents the minor version
// version(1) would mean version 3.1
type version int

func (v version) String() string {
	return fmt.Sprintf("3.%d", int(v))
}

func versionFromString(v string) (version, error) {
	switch v {
	case "3.0":
		return version(0), nil
	case "3.1":
		return version(1), nil
	default:
		return version(0), fmt.Errorf("cvss v3 version %s not supported", v)
	}
}

// Vector represents a CVSSv3 vector, holds all metrics inside (base, temporal and environmental)
type Vector struct {
	version version
	BaseMetrics
	TemporalMetrics
	EnvironmentalMetrics
}

// For some metrics, "undefined" is equivalent to specifying another value. eg.
// E:X is equivalent to E:H.
var undefinedEquivalent = map[string]string{
	// Temporal metrics
	"E":  "H",
	"RL": "U",
	"RC": "C",
	// Environmental metrics
	"CR": "M",
	"IR": "M",
	"AR": "M",
}

func equivalent(metric, value string) string {
	if value != "X" {
		return value
	}
	e, ok := undefinedEquivalent[metric]
	if !ok {
		return value
	}
	return e
}

// Equal returns true if o represents the same vector as v.
//
// Note that the definition of equal here means that two vectors with different
// string representations can still be equal. For instance RL:X is defined as
// the same as RL:U. From the spec:
//
//   Assigning this value indicates there is insufficient information to choose
//   one of the other values, and has no impact on the overall Temporal Score,
//   i.e., it has the same effect on scoring as assigning Unavailable.
//
// https://www.first.org/cvss/specification-document:
func (v Vector) Equal(o Vector) bool {
	vDefs := v.definables()
	oDefs := o.definables()

	for _, metric := range order {
		a := equivalent(metric, vDefs[metric].String())
		b := equivalent(metric, oDefs[metric].String())

		if a != b {
			return false
		}
	}
	return true
}

// String returns this vectors representation as a string
// it shouldn't depend on the order of metrics
func (v Vector) String() string {
	var sb strings.Builder
	fmt.Fprintf(&sb, "%s%s/", prefix, v.version)

	defineables := v.definables()

	first := true
	for _, metric := range order {
		def := defineables[metric]
		if !def.defined() {
			continue
		}
		if !first {
			fmt.Fprint(&sb, partSeparator)
		} else {
			first = false
		}
		fmt.Fprintf(&sb, "%s%s%s", metric, metricSeparator, def)
	}

	return sb.String()
}

// VectorFromString will parse a string into a Vector, or return an error if it can't be parsed
func VectorFromString(str string) (Vector, error) {
	var v Vector

	// check for prefix and trim it
	str = strings.ToUpper(str)
	if !strings.HasPrefix(str, prefix) {
		return v, fmt.Errorf("vector missing %q prefix: %q", prefix, str)
	}
	str = strings.TrimPrefix(str, prefix)

	// extract version
	slashIdx := strings.IndexByte(str, '/')
	var err error
	if v.version, err = versionFromString(str[:slashIdx]); err != nil {
		return v, err
	}
	str = str[slashIdx+1:]

	// parse all metrics
	parseables := v.parseables()

	for _, part := range strings.Split(str, partSeparator) {
		tmp := strings.Split(part, metricSeparator)
		if len(tmp) != 2 {
			return v, fmt.Errorf("need two values separated by %s, got %q", metricSeparator, part)
		}

		metric, value := tmp[0], tmp[1]
		if p, ok := parseables[metric]; !ok {
			return v, fmt.Errorf("undefined metric %s with value %s", metric, value)
		} else if err := p.parse(value); err != nil {
			return v, fmt.Errorf("error occurred while parsing metric %s: %v", metric, err)
		}
	}

	return v, nil
}

// Absorb will override only metrics in the current vector from the one given which are defined
// If the other vector specifies only a single metric with all others undefined, the resulting
// vector will contain all metrics it previously did, with only the new one overriden
func (v *Vector) Absorb(other Vector) {
	parseables := v.parseables()
	for metric, defineable := range other.definables() {
		if defineable.defined() {
			parseables[metric].parse(defineable.String())
		}
	}
}

// AbsorbIfDefined is like Absorb but will not override vector components that
// are not present in v.
func (v *Vector) AbsorbIfDefined(other Vector) {
	parseables := v.parseables()
	old := v.definables()
	for metric, defineable := range other.definables() {
		if old[metric].defined() && defineable.defined() {
			parseables[metric].parse(defineable.String())
		}
	}
}

// helpers

var order = []string{"AV", "AC", "PR", "UI", "S", "C", "I", "A", "E", "RL", "RC", "CR", "IR", "AR", "MAV", "MAC", "MPR", "MUI", "MS", "MC", "MI", "MA", "ME", "MRL", "MRC"}

type defineable interface {
	defined() bool
	String() string
}

type parseable interface {
	parse(string) error
}

func (v *Vector) definables() map[string]defineable {
	return map[string]defineable{
		// base metrics
		"AV": v.BaseMetrics.AttackVector,
		"AC": v.BaseMetrics.AttackComplexity,
		"PR": v.BaseMetrics.PrivilegesRequired,
		"UI": v.BaseMetrics.UserInteraction,
		"S":  v.BaseMetrics.Scope,
		"C":  v.BaseMetrics.Confidentiality,
		"I":  v.BaseMetrics.Integrity,
		"A":  v.BaseMetrics.Availability,
		// temporal metrics
		"E":  v.TemporalMetrics.ExploitCodeMaturity,
		"RL": v.TemporalMetrics.RemediationLevel,
		"RC": v.TemporalMetrics.ReportConfidence,
		// environmental metrics
		"CR":  v.EnvironmentalMetrics.ConfidentialityRequirement,
		"IR":  v.EnvironmentalMetrics.IntegrityRequirement,
		"AR":  v.EnvironmentalMetrics.AvailabilityRequirement,
		"MAV": v.EnvironmentalMetrics.ModifiedAttackVector,
		"MAC": v.EnvironmentalMetrics.ModifiedAttackComplexity,
		"MPR": v.EnvironmentalMetrics.ModifiedPrivilegesRequired,
		"MUI": v.EnvironmentalMetrics.ModifiedUserInteraction,
		"MS":  v.EnvironmentalMetrics.ModifiedScope,
		"MC":  v.EnvironmentalMetrics.ModifiedConfidentiality,
		"MI":  v.EnvironmentalMetrics.ModifiedIntegrity,
		"MA":  v.EnvironmentalMetrics.ModifiedAvailability,
		"ME":  v.EnvironmentalMetrics.ModifiedExploitCodeMaturity,
		"MRL": v.EnvironmentalMetrics.ModifiedRemediationLevel,
		"MRC": v.EnvironmentalMetrics.ModifiedReportConfidence,
	}
}

func (v *Vector) parseables() map[string]parseable {
	return map[string]parseable{
		// base metrics
		"AV": &v.BaseMetrics.AttackVector,
		"AC": &v.BaseMetrics.AttackComplexity,
		"PR": &v.BaseMetrics.PrivilegesRequired,
		"UI": &v.BaseMetrics.UserInteraction,
		"S":  &v.BaseMetrics.Scope,
		"C":  &v.BaseMetrics.Confidentiality,
		"I":  &v.BaseMetrics.Integrity,
		"A":  &v.BaseMetrics.Availability,
		// temporal metrics
		"E":  &v.TemporalMetrics.ExploitCodeMaturity,
		"RL": &v.TemporalMetrics.RemediationLevel,
		"RC": &v.TemporalMetrics.ReportConfidence,
		// environmental metrics
		"CR":  &v.EnvironmentalMetrics.ConfidentialityRequirement,
		"IR":  &v.EnvironmentalMetrics.IntegrityRequirement,
		"AR":  &v.EnvironmentalMetrics.AvailabilityRequirement,
		"MAV": &v.EnvironmentalMetrics.ModifiedAttackVector,
		"MAC": &v.EnvironmentalMetrics.ModifiedAttackComplexity,
		"MPR": &v.EnvironmentalMetrics.ModifiedPrivilegesRequired,
		"MUI": &v.EnvironmentalMetrics.ModifiedUserInteraction,
		"MS":  &v.EnvironmentalMetrics.ModifiedScope,
		"MC":  &v.EnvironmentalMetrics.ModifiedConfidentiality,
		"MI":  &v.EnvironmentalMetrics.ModifiedIntegrity,
		"MA":  &v.EnvironmentalMetrics.ModifiedAvailability,
		"ME":  &v.EnvironmentalMetrics.ModifiedExploitCodeMaturity,
		"MRL": &v.EnvironmentalMetrics.ModifiedRemediationLevel,
		"MRC": &v.EnvironmentalMetrics.ModifiedReportConfidence,
	}
}
