package osvschema

import (
	"encoding/json"
	"time"
)

// Package describes the affected code library or command provided by the
// package.
//
// See: https://ossf.github.io/osv-schema/#affectedpackage-field
type Package struct {
	Ecosystem string `json:"ecosystem"      yaml:"ecosystem"`
	Name      string `json:"name"           yaml:"name"`
	Purl      string `json:"purl,omitempty" yaml:"purl,omitempty"`
}

// Event describes a single version that either:
//
//   - Introduces a vulnerability: {"introduced": string}
//   - Fixes a vulnerability: {"fixed": string}
//   - Describes the last known affected version: {"last_affected": string}
//   - Sets an upper limit on the range being described: {"limit": string}
//
// Event instances form part of a “timeline” of status changes for the affected
// package described by the Affected struct.
//
// See: https://ossf.github.io/osv-schema/#affectedrangesevents-fields
type Event struct {
	Introduced   string `json:"introduced,omitempty"    yaml:"introduced,omitempty"`
	Fixed        string `json:"fixed,omitempty"         yaml:"fixed,omitempty"`
	LastAffected string `json:"last_affected,omitempty" yaml:"last_affected,omitempty"`
	Limit        string `json:"limit,omitempty"         yaml:"limit,omitempty"`
}

// Range describes the affected range of given version for a specific package.
//
// See: https://ossf.github.io/osv-schema/#affectedranges-field
type Range struct {
	Type             RangeType              `json:"type"                        yaml:"type"`
	Events           []Event                `json:"events"                      yaml:"events"`
	Repo             string                 `json:"repo,omitempty"              yaml:"repo,omitempty"`
	DatabaseSpecific map[string]interface{} `json:"database_specific,omitempty" yaml:"database_specific,omitempty"`
}

// Severity describes the severity of a vulnerability for an affected
// package using one or more quantitative scoring methods.
//
// See: https://ossf.github.io/osv-schema/#severity-field
type Severity struct {
	Type  SeverityType `json:"type"  yaml:"type"`
	Score string       `json:"score" yaml:"score"`
}

// Affected describes an affected package version, meaning one instance that
// contains the vulnerability.
//
// See: https://ossf.github.io/osv-schema/#affected-fields
type Affected struct {
	Package           Package                `json:"package,omitempty"            yaml:"package,omitempty"`
	Severity          []Severity             `json:"severity,omitempty"           yaml:"severity,omitempty"`
	Ranges            []Range                `json:"ranges,omitempty"             yaml:"ranges,omitempty"`
	Versions          []string               `json:"versions,omitempty"           yaml:"versions,omitempty"`
	DatabaseSpecific  map[string]interface{} `json:"database_specific,omitempty"  yaml:"database_specific,omitempty"`
	EcosystemSpecific map[string]interface{} `json:"ecosystem_specific,omitempty" yaml:"ecosystem_specific,omitempty"`
}

// Reference links to additional information, advisories, issue tracker entries,
// and so on about the vulnerability itself.
//
// See: https://ossf.github.io/osv-schema/#references-field
type Reference struct {
	Type ReferenceType `json:"type" yaml:"type"`
	URL  string        `json:"url"  yaml:"url"`
}

// Credit describes who to give credit to for the discovery, confirmation, patch, or other events
// in the life cycle of a vulnerability.
//
// See: https://ossf.github.io/osv-schema/#credits-fields
type Credit struct {
	Name    string     `json:"name"              yaml:"name"`
	Type    CreditType `json:"type,omitempty"    yaml:"type,omitempty"`
	Contact []string   `json:"contact,omitempty" yaml:"contact,omitempty"`
}

// Vulnerability is the core Open Source Vulnerability (OSV) data type.
//
// The full documentation for the schema is available at
// https://ossf.github.io/osv-schema.
type Vulnerability struct {
	SchemaVersion    string                 `json:"schema_version,omitempty"    yaml:"schema_version,omitempty"`
	ID               string                 `json:"id"                          yaml:"id"`
	Modified         time.Time              `json:"modified"                    yaml:"modified"`
	Published        time.Time              `json:"published,omitempty"         yaml:"published,omitempty"`
	Withdrawn        time.Time              `json:"withdrawn,omitempty"         yaml:"withdrawn,omitempty"`
	Aliases          []string               `json:"aliases,omitempty"           yaml:"aliases,omitempty"`
	Related          []string               `json:"related,omitempty"           yaml:"related,omitempty"`
	Upstream         []string               `json:"upstream,omitempty"          yaml:"upstream,omitempty"`
	Summary          string                 `json:"summary,omitempty"           yaml:"summary,omitempty"`
	Details          string                 `json:"details,omitempty"           yaml:"details,omitempty"`
	Severity         []Severity             `json:"severity,omitempty"          yaml:"severity,omitempty"`
	Affected         []Affected             `json:"affected,omitempty"          yaml:"affected,omitempty"`
	References       []Reference            `json:"references,omitempty"        yaml:"references,omitempty"`
	Credits          []Credit               `json:"credits,omitempty"           yaml:"credits,omitempty"`
	DatabaseSpecific map[string]interface{} `json:"database_specific,omitempty" yaml:"database_specific,omitempty"`
}

// MarshalJSON implements the json.Marshaler interface.
//
// This method ensures Package is only present if it is not equal to the zero value.
// This is achieved by embedding the Affected struct with a pointer to Package used
// to populate the "package" key in the JSON object.
func (a Affected) MarshalJSON() ([]byte, error) {
	type rawAffected Affected // alias Affected to avoid recursion during Marshal
	type wrapper struct {
		Package *Package `json:"package,omitempty"`
		rawAffected
	}
	raw := wrapper{rawAffected: rawAffected(a)}
	if a.Package == (Package{}) {
		raw.Package = nil
	} else {
		raw.Package = &(a.Package)
	}

	return json.Marshal(raw)
}

// MarshalJSON implements the json.Marshaler interface.
//
// This method ensures all times are formatted correctly according to the schema.
func (v Vulnerability) MarshalJSON() ([]byte, error) {
	type rawVulnerability Vulnerability // alias Vulnerability to avoid recursion during Marshal
	type wrapper struct {
		Modified  string `json:"modified"`
		Published string `json:"published,omitempty"`
		Withdrawn string `json:"withdrawn,omitempty"`
		rawVulnerability
	}
	raw := wrapper{rawVulnerability: rawVulnerability(v)}
	raw.Modified = v.Modified.UTC().Format(time.RFC3339)
	if !v.Published.IsZero() {
		raw.Published = v.Published.UTC().Format(time.RFC3339)
	}
	if !v.Withdrawn.IsZero() {
		raw.Withdrawn = v.Withdrawn.UTC().Format(time.RFC3339)
	}

	return json.Marshal(raw)
}

// MarshalYAML implements the yaml.Marshaler interface.
//
// This method ensures times all times are formatted correctly according to the schema.
func (v Vulnerability) MarshalYAML() (interface{}, error) {
	type rawVulnerability Vulnerability // alias Vulnerability to avoid recursion during Marshal
	raw := rawVulnerability(v)
	if !v.Modified.IsZero() {
		raw.Modified = v.Modified.UTC()
	}
	if !v.Published.IsZero() {
		raw.Published = v.Published.UTC()
	}
	if !v.Withdrawn.IsZero() {
		raw.Withdrawn = v.Withdrawn.UTC()
	}

	return raw, nil
}
