// Package imodels defines internal models for osv-scanner.
package imodels

import (
	"fmt"
	"path/filepath"
	"strconv"
	"strings"
	"sync"

	"github.com/google/osv-scalibr/converter"
	"github.com/google/osv-scalibr/extractor"
	archivemetadata "github.com/google/osv-scalibr/extractor/filesystem/language/java/archive/metadata"
	apkmetadata "github.com/google/osv-scalibr/extractor/filesystem/os/apk/metadata"
	dpkgmetadata "github.com/google/osv-scalibr/extractor/filesystem/os/dpkg/metadata"
	rpmmetadata "github.com/google/osv-scalibr/extractor/filesystem/os/rpm/metadata"
	"github.com/google/osv-scalibr/inventory/osvecosystem"
	"github.com/google/osv-scanner/v2/internal/cachedregexp"
	"github.com/google/osv-scanner/v2/internal/cmdlogger"
	"github.com/google/osv-scanner/v2/internal/scalibrextract/language/osv/osvscannerjson"
	"github.com/google/osv-scanner/v2/internal/scalibrextract/vcs/gitrepo"
	"github.com/google/osv-scanner/v2/internal/scalibrplugin"
	"github.com/google/osv-scanner/v2/internal/utility/purl"
	"github.com/google/osv-scanner/v2/internal/utility/semverlike"

	scalibrosv "github.com/google/osv-scalibr/extractor/filesystem/osv"
	"github.com/google/osv-scanner/v2/pkg/models"
	"github.com/ossf/osv-schema/bindings/go/osvconstants"
)

var gitExtractors = map[string]struct{}{
	gitrepo.Name: {},
}

// todo: SBOM special case, to be removed after PURL to ESI conversion within each extractor is complete
var cache = sync.Map{} // map[*extractor.Package]*models.PackageInfo

func toCachedPackageInfo(pkg *extractor.Package) *models.PackageInfo {
	if SourceType(pkg) != models.SourceTypeSBOM {
		return nil
	}

	v, ok := cache.Load(pkg)

	if !ok {
		purlStruct := converter.ToPURL(pkg)

		if purlStruct == nil {
			return nil
		}

		purlCache, _ := purl.ToPackage(purlStruct.String())
		cache.Store(pkg, &purlCache)

		return &purlCache
	}

	if v == nil {
		return nil
	}

	return v.(*models.PackageInfo)
}

// Name patches the SCALIBR package name to something used by OSV.
// This does not affect matching. It is only used for reporting and filtering.
func Name(pkg *extractor.Package) string {
	// --- Make specific patches to names as necessary ---
	// Patch Go package to stdlib
	if Ecosystem(pkg).Ecosystem == osvconstants.EcosystemGo && pkg.Name == "go" {
		return "stdlib"
	}

	// Patch python package names to be normalized
	if Ecosystem(pkg).Ecosystem == osvconstants.EcosystemPyPI {
		// per https://peps.python.org/pep-0503/#normalized-names
		return strings.ToLower(cachedregexp.MustCompile(`[-_.]+`).ReplaceAllLiteralString(pkg.Name, "-"))
	}

	// Patch Maven archive extractor package names
	if metadata, ok := pkg.Metadata.(*archivemetadata.Metadata); ok {
		if metadata.ArtifactID != "" && metadata.GroupID != "" {
			return metadata.GroupID + ":" + metadata.ArtifactID
		}
	}

	// --- OS metadata ---
	if metadata, ok := pkg.Metadata.(*dpkgmetadata.Metadata); ok {
		// Debian uses source name on osv.dev
		// (fallback to using the normal name if source name is empty)
		if metadata.SourceName != "" {
			return metadata.SourceName
		}
	}

	if metadata, ok := pkg.Metadata.(*apkmetadata.Metadata); ok {
		if metadata.OriginName != "" {
			return metadata.OriginName
		}
	}

	if Ecosystem(pkg).String() == "GIT" && pkg.SourceCode != nil && pkg.SourceCode.Repo != "" {
		return pkg.SourceCode.Repo
	}

	return pkg.Name
}

func Ecosystem(pkg *extractor.Package) osvecosystem.Parsed {
	eco := pkg.Ecosystem()

	if metadata, ok := pkg.Metadata.(*osvscannerjson.Metadata); ok {
		newEco, err := osvecosystem.Parse(metadata.Ecosystem)
		if err != nil {
			cmdlogger.Warnf("Warning: error parsing osvscanner.json ecosystem: %s", err.Error())
			return eco
		}

		if !newEco.IsEmpty() {
			eco = newEco
		}
	}

	return eco
}

func Version(pkg *extractor.Package) string {
	// TODO(v2): SBOM special case, to be removed after PURL to ESI conversion within each extractor is complete
	if purlCache := toCachedPackageInfo(pkg); purlCache != nil {
		return purlCache.Version
	}

	// Assume Go stdlib patch version as the latest version
	//
	// This is done because go1.20 and earlier do not support patch
	// version in go.mod file, and will fail to build.
	//
	// However, if we assume patch version as .0, this will cause a lot of
	// false positives. This compromise still allows osv-scanner to pick up
	// when the user is using a minor version that is out-of-support.
	if Ecosystem(pkg).Ecosystem == osvconstants.EcosystemGo && Name(pkg) == "stdlib" {
		v := semverlike.ParseSemverLikeVersion(pkg.Version, 3)
		if len(v.Components) == 2 {
			return fmt.Sprintf(
				"%d.%d.%d",
				v.Components.Fetch(0),
				v.Components.Fetch(1),
				99,
			)
		}
	}

	// scalibr stores the RPM epoch separately from the version string. For
	// ecosystems whose OSV records encode it, prepend the epoch when non-zero
	// (e.g. "3.2.2-7.el9_6" -> "1:3.2.2-7.el9_6"); otherwise a missing epoch is
	// read as 0 and already-fixed advisories are reported as unfixed.
	if m, ok := pkg.Metadata.(*rpmmetadata.Metadata); ok && m.Epoch > 0 && ecosystemEncodesEpoch(Ecosystem(pkg).String()) {
		return strconv.Itoa(m.Epoch) + ":" + pkg.Version
	}

	return pkg.Version
}

// rhelFamilyEpochEcosystems lists the RPM ecosystems whose OSV records encode
// the package epoch (verified against api.osv.dev). Others (e.g. openEuler)
// store epoch-less records, so prepending an epoch there would hide real
// vulnerabilities; add entries only once epoch-encoding is confirmed.
var rhelFamilyEpochEcosystems = map[string]bool{
	"Red Hat":     true,
	"AlmaLinux":   true,
	"Rocky Linux": true,
}

// ecosystemEncodesEpoch reports whether the ecosystem's OSV records carry the
// RPM epoch, so its version must be epoch-qualified to compare correctly.
func ecosystemEncodesEpoch(ecosystem string) bool {
	distro, _, _ := strings.Cut(ecosystem, ":")
	return rhelFamilyEpochEcosystems[distro]
}

func Location(pkg *extractor.Package) string {
	// Use the scan root, defaulting to / for virtual filesystems (e.g. container images)
	scanRoot := pkg.ScanRoot
	if scanRoot == "" {
		scanRoot = "/"
	}

	return filepath.Join(scanRoot, pkg.Location.PathOrEmpty())
}

func Commit(pkg *extractor.Package) string {
	if pkg.SourceCode != nil {
		return pkg.SourceCode.Commit
	}

	return ""
}

func SourceType(pkg *extractor.Package) models.SourceType {
	for _, extractorName := range pkg.Plugins {
		if strings.HasPrefix(extractorName, "os/") {
			return models.SourceTypeOSPackage
		} else if _, ok := scalibrplugin.ExtractorPresets["sbom"][extractorName]; ok {
			return models.SourceTypeSBOM
		} else if _, ok := gitExtractors[extractorName]; ok {
			return models.SourceTypeGit
		} else if _, ok := scalibrplugin.ExtractorPresets["artifact"][extractorName]; ok {
			return models.SourceTypeArtifact
		} else if _, ok := scalibrplugin.ExtractorPresets["lockfile"][extractorName]; ok {
			return models.SourceTypeProjectPackage
		}
	}

	return models.SourceTypeUnknown
}

func DepGroups(pkg *extractor.Package) []string {
	if dg, ok := pkg.Metadata.(scalibrosv.DepGroups); ok {
		return dg.DepGroups()
	}

	return []string{}
}

func OSPackageName(pkg *extractor.Package) string {
	if metadata, ok := pkg.Metadata.(*apkmetadata.Metadata); ok {
		return metadata.PackageName
	}
	if metadata, ok := pkg.Metadata.(*dpkgmetadata.Metadata); ok {
		return metadata.PackageName
	}
	if metadata, ok := pkg.Metadata.(*rpmmetadata.Metadata); ok {
		return metadata.PackageName
	}

	return ""
}
