// Package vulndb matches installed packages against distribution security
// advisories.
//
// Version comparison is bought rather than written. Distribution version
// ordering is subtle in ways that are invisible until they are wrong: dpkg has
// epochs and sorts "~" before the empty string, rpmvercmp has its own segment
// rules and treats "~" and "^" differently again, and any ordering that falls
// back on string comparison puts 1.10 before 1.9. Every one of those mistakes
// produces a false negative — a vulnerable host reported clean — which is the
// failure nobody notices.
package vulndb

import (
	"errors"
	"fmt"

	apk "github.com/knqyf263/go-apk-version"
	deb "github.com/knqyf263/go-deb-version"
	rpm "github.com/knqyf263/go-rpm-version"
)

// ErrUnsupportedFamily means we hold no comparator for this distribution, and
// therefore cannot answer whether it is vulnerable. Callers must surface this
// as "unsupported" and must never treat it as "not vulnerable".
var ErrUnsupportedFamily = errors.New("unsupported OS family")

// LessThan reports whether version a sorts before version b under the ordering
// rules of the given OS family.
//
// An unparseable or empty version is an error, never a quiet false. False here
// means "not vulnerable", which is the dangerous direction to guess in.
func LessThan(family, a, b string) (bool, error) {
	switch family {
	case "debian", "ubuntu":
		if a == "" || b == "" {
			return false, fmt.Errorf("empty deb version (a=%q b=%q)", a, b)
		}
		va, err := deb.NewVersion(a)
		if err != nil {
			return false, fmt.Errorf("parse deb version %q: %w", a, err)
		}
		vb, err := deb.NewVersion(b)
		if err != nil {
			return false, fmt.Errorf("parse deb version %q: %w", b, err)
		}
		return va.LessThan(vb), nil

	case "redhat", "centos", "rocky", "alma", "amazon", "oracle", "suse", "opensuse", "sles":
		// go-rpm-version does not error; rpmvercmp is defined over arbitrary
		// strings. Guard empties so a missing version cannot read as equal.
		if a == "" || b == "" {
			return false, fmt.Errorf("empty rpm version (a=%q b=%q)", a, b)
		}
		return rpm.NewVersion(a).LessThan(rpm.NewVersion(b)), nil

	case "alpine":
		if a == "" || b == "" {
			return false, fmt.Errorf("empty apk version (a=%q b=%q)", a, b)
		}
		va, err := apk.NewVersion(a)
		if err != nil {
			return false, fmt.Errorf("parse apk version %q: %w", a, err)
		}
		vb, err := apk.NewVersion(b)
		if err != nil {
			return false, fmt.Errorf("parse apk version %q: %w", b, err)
		}
		return va.LessThan(vb), nil

	default:
		return false, fmt.Errorf("%w: %s", ErrUnsupportedFamily, family)
	}
}
