package helpers

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/CycloneDX/cyclonedx-go"

	"github.com/anchore/packageurl-go"
	"github.com/anchore/syft/internal/packagemetadata"
	"github.com/anchore/syft/syft/file"
	"github.com/anchore/syft/syft/format/internal"
	"github.com/anchore/syft/syft/pkg"
)

func EncodeComponent(p pkg.Package, supplier string, locationSorter func(a, b file.Location) int) cyclonedx.Component {
	props := EncodeProperties(p, "syft:package")

	if p.Metadata != nil {
		// encode the metadataType as a property, something that doesn't exist on the core model
		props = append(props, cyclonedx.Property{
			Name:  "syft:package:metadataType",
			Value: packagemetadata.JSONName(p.Metadata),
		})
	}

	props = append(props, encodeCPEs(p)...)
	locations := p.Locations.ToSlice(locationSorter)
	if len(locations) > 0 {
		props = append(props, EncodeProperties(locations, "syft:location")...)
	}
	if hasMetadata(p) {
		props = append(props, EncodeProperties(p.Metadata, "syft:metadata")...)
	}

	var properties *[]cyclonedx.Property
	if len(props) > 0 {
		properties = &props
	}

	componentType := cyclonedx.ComponentTypeLibrary
	switch p.Type {
	case pkg.BinaryPkg:
		componentType = cyclonedx.ComponentTypeApplication
	case pkg.ModelPkg:
		componentType = cyclonedx.ComponentTypeMachineLearningModel
	}

	return cyclonedx.Component{
		Type:               componentType,
		Name:               p.Name,
		Group:              encodeGroup(p),
		Version:            p.Version,
		Supplier:           encodeSupplier(p, supplier),
		PackageURL:         p.PURL,
		Licenses:           encodeLicenses(p),
		CPE:                encodeSingleCPE(p),
		Author:             encodeAuthor(p),
		Publisher:          encodePublisher(p),
		Description:        encodeDescription(p),
		ExternalReferences: encodeExternalReferences(p),
		Properties:         properties,
		BOMRef:             DeriveBomRef(p),
	}
}

// TODO: we eventually want to update this so that we can read "supplier" from different syft metadata
func encodeSupplier(_ pkg.Package, sbomSupplier string) *cyclonedx.OrganizationalEntity {
	if sbomSupplier != "" {
		return &cyclonedx.OrganizationalEntity{
			Name: sbomSupplier,
		}
	}
	return nil
}

func DeriveBomRef(p pkg.Package) string {
	// try and parse the PURL if possible and append syft id to it, to make
	// the purl unique in the BOM.
	// TODO: In the future we may want to dedupe by PURL and combine components with
	// the same PURL while preserving their unique metadata.
	if parsedPURL, err := packageurl.FromString(p.PURL); err == nil {
		parsedPURL.Qualifiers = append(parsedPURL.Qualifiers, packageurl.Qualifier{Key: "package-id", Value: string(p.ID())})
		return parsedPURL.ToString()
	}
	// fallback is to use strictly the ID if there is no valid pURL
	return string(p.ID())
}

func hasMetadata(p pkg.Package) bool {
	return p.Metadata != nil
}

func decodeComponent(c *cyclonedx.Component) *pkg.Package {
	values := map[string]string{}
	if c.Properties != nil {
		for _, p := range *c.Properties {
			values[p.Name] = p.Value
		}
	}

	p := &pkg.Package{
		Version:   c.Version,
		Locations: decodeLocations(values),
		Licenses:  pkg.NewLicenseSet(decodeLicenses(c)...),
		CPEs:      decodeCPEs(c),
	}

	// note: this may write in syft package type information
	DecodeInto(p, values, "syft:package", CycloneDXFields)

	metadataType := values["syft:package:metadataType"]

	p.Metadata = decodePackageMetadata(values, c, metadataType)

	// this will either use the purl from the component or generate a new one based off of any type information
	// that was decoded above.
	p.PURL = getPURL(c, p.Type)

	if p.Type == "" {
		p.Type = pkg.TypeFromPURL(p.PURL)
	}

	setPackageName(p, c)

	internal.Backfill(p)
	p.SetID()

	return p
}

func getPURL(c *cyclonedx.Component, ty pkg.Type) string {
	if c.PackageURL != "" {
		// if there is a purl that where the namespace does not match the group information, we may
		// accidentally drop group. We should consider adding group as a top-level syft package field.
		return c.PackageURL
	}

	if strings.HasPrefix(c.BOMRef, "pkg:") {
		// the bomref is a purl, so try to use that as the purl
		_, err := packageurl.FromString(c.BOMRef)
		if err == nil {
			return c.BOMRef
		}
	}

	if ty == "" {
		return ""
	}

	tyStr := ty.PackageURLType()
	switch tyStr {
	case "", packageurl.TypeGeneric:
		return ""
	}

	purl := packageurl.PackageURL{
		Type:      tyStr,
		Namespace: c.Group,
		Name:      c.Name,
		Version:   c.Version,
	}

	return purl.ToString()
}

//nolint:gocognit
func setPackageName(p *pkg.Package, c *cyclonedx.Component) {
	name := c.Name
	if c.Group != "" {
		switch p.Type {
		case pkg.JavaPkg:
			if p.Metadata == nil {
				p.Metadata = pkg.JavaArchive{}
			}
			var pomProperties *pkg.JavaPomProperties
			javaMetadata, ok := p.Metadata.(pkg.JavaArchive)
			if ok {
				pomProperties = javaMetadata.PomProperties
				if pomProperties == nil {
					pomProperties = &pkg.JavaPomProperties{}
					javaMetadata.PomProperties = pomProperties
					p.Metadata = javaMetadata
				}
			}
			if pomProperties != nil {
				if pomProperties.ArtifactID == "" {
					pomProperties.ArtifactID = c.Name
				}
				if pomProperties.GroupID == "" {
					pomProperties.GroupID = c.Group
				}
				if pomProperties.Version == "" {
					pomProperties.Version = p.Version
				}
			}
		default:
			if !internal.NameExcludesPurlNamespace(p.Type.PackageURLType()) {
				name = fmt.Sprintf("%s/%s", c.Group, name)
			}
		}
	}
	p.Name = name
}

func decodeLocations(vals map[string]string) file.LocationSet {
	v := Decode(reflect.TypeFor[[]file.Location](), vals, "syft:location", CycloneDXFields)
	out, ok := v.([]file.Location)
	if !ok {
		out = nil
	}
	return file.NewLocationSet(out...)
}

func decodePackageMetadata(vals map[string]string, c *cyclonedx.Component, typeName string) any {
	if typeName != "" && c.Properties != nil {
		metadataType := packagemetadata.ReflectTypeFromJSONName(typeName)
		if metadataType == nil {
			return nil
		}
		metaPtrTyp := reflect.PointerTo(metadataType)
		metaPtr := Decode(metaPtrTyp, vals, "syft:metadata", CycloneDXFields)

		// Map all explicit metadata properties
		decodeAuthor(c.Author, metaPtr)
		decodeGroup(c.Group, metaPtr)
		decodePublisher(c.Publisher, metaPtr)
		decodeDescription(c.Description, metaPtr)
		decodeExternalReferences(c, metaPtr)

		// return the actual interface{} -> struct ... not interface{} -> *struct
		return PtrToStruct(metaPtr)
	}

	return nil
}
