package vulndb

import (
	"log"
	"strings"

	trivydb "github.com/aquasecurity/trivy-db/pkg/db"
	trivytypes "github.com/aquasecurity/trivy-db/pkg/types"
)

// Advisory is one fixed-version statement for one source package.
type Advisory struct {
	CVEID string
	// FixedVersion empty means no vendor fix has been published. It is a real
	// state, not an absence of data, and callers must treat it as vulnerable.
	FixedVersion string
	Severity     string
	// Status is filled by trivy-db ONLY when FixedVersion is empty — when there
	// is a fix, "fixed" is the obvious state and the field is left zero. It is
	// what separates "the vendor confirms this package is affected and has not
	// fixed it" from "nobody has looked yet".
	Status string
}

// VulnInfo is the CVE's own metadata, shared across every server it affects.
type VulnInfo struct {
	Title      string
	Severity   string
	CVSSScore  float64
	References []string
}

// Store reads a pulled trivy-db.
type Store struct {
	cfg trivydb.Config
}

// Open opens the database in dir. trivy-db expects the directory, not the file:
// it appends "trivy.db" itself.
func Open(dir string) (*Store, error) {
	if err := trivydb.Init(dir); err != nil {
		log.Printf("vulndb: open %s: %v", dir, err)
		return nil, err
	}
	log.Printf("vulndb: opened database in %s", dir)
	return &Store{cfg: trivydb.Config{}}, nil
}

func (s *Store) Close() error { return trivydb.Close() }

// Advisories returns every advisory for a source package in a bucket.
func (s *Store) Advisories(bucket, srcName string) ([]Advisory, error) {
	raw, err := s.cfg.GetAdvisories(bucket, srcName)
	if err != nil {
		return nil, err
	}
	out := make([]Advisory, 0, len(raw))
	for _, a := range raw {
		out = append(out, Advisory{
			CVEID:        a.VulnerabilityID,
			FixedVersion: a.FixedVersion,
			// Advisory.Severity is trivy's numeric Severity type, unlike
			// Vulnerability.Severity which is a string. They are genuinely
			// different types in trivy-db, not an inconsistency here.
			Severity: severityFromLevel(a.Severity),
			Status:   a.Status.String(),
		})
	}
	return out, nil
}

// Vulnerability returns a CVE's shared metadata.
func (s *Store) Vulnerability(cveID string) (VulnInfo, error) {
	v, err := s.cfg.GetVulnerability(cveID)
	if err != nil {
		return VulnInfo{}, err
	}
	return VulnInfo{
		Title:      v.Title,
		Severity:   resolveSeverity(v),
		CVSSScore:  topCVSS(v),
		References: v.References,
	}, nil
}

// resolveSeverity picks a CVE's severity: vendor, then NVD, then unknown.
//
// Never invented. This will surface as "why is this critical CVE marked low":
// Debian and Red Hat routinely downgrade an NVD score because the vulnerable
// path is not reachable in their build, and their rating is the accurate one
// for that package. Where several vendors disagree the highest wins, because
// under-reporting a vulnerability is the worse mistake.
func resolveSeverity(v trivytypes.Vulnerability) string {
	best := 0
	for _, sev := range v.VendorSeverity {
		if int(sev) > best {
			best = int(sev)
		}
	}
	if best > 0 {
		return severityFromLevel(trivytypes.Severity(best))
	}

	// Vulnerability.Severity is the deprecated NVD-derived string. Used only as
	// the fallback, which is exactly what it is good for.
	if s := strings.ToLower(strings.TrimSpace(v.Severity)); s != "" && s != "unknown" {
		return s
	}
	return "unknown"
}

// topCVSS returns the highest V3 score any source published, or 0.
func topCVSS(v trivytypes.Vulnerability) float64 {
	var top float64
	for _, c := range v.CVSS {
		if c.V3Score > top {
			top = c.V3Score
		}
	}
	return top
}

// severityFromLevel maps trivy-db's numeric severity onto our lowercase
// strings. The names are fixed by models.Severity* and must stay in step.
func severityFromLevel(n trivytypes.Severity) string {
	switch int(n) {
	case 4:
		return "critical"
	case 3:
		return "high"
	case 2:
		return "medium"
	case 1:
		return "low"
	default:
		return "unknown"
	}
}
