package format

import (
	"bytes"
	"fmt"
	"regexp"
	"sort"
	"strings"

	"github.com/scylladb/go-set/strset"

	"github.com/anchore/syft/internal/log"
	"github.com/anchore/syft/syft/sbom"
)

type EncoderCollection struct {
	defaultVersions map[sbom.FormatID]string
	encoders        []sbom.FormatEncoder
}

func NewEncoderCollection(encoders ...sbom.FormatEncoder) *EncoderCollection {
	defaultVersions := map[sbom.FormatID]string{}
	for _, encoder := range encoders {
		if defaultVersion, ok := encoder.(defaultVersionIndicator); ok {
			if defaultVersion.DefaultVersion() {
				defaultVersions[encoder.ID()] = encoder.Version()
			}
		}
	}
	return &EncoderCollection{
		defaultVersions: defaultVersions,
		encoders:        encoders,
	}
}

// IDs returns all format IDs represented in the collection.
func (e EncoderCollection) IDs() []sbom.FormatID {
	idSet := strset.New()
	for _, f := range e.encoders {
		idSet.Add(string(f.ID()))
	}

	idList := idSet.List()
	sort.Strings(idList)

	var ids []sbom.FormatID
	for _, id := range idList {
		ids = append(ids, sbom.FormatID(id))
	}

	return ids
}

// NameVersions returns all formats that are supported by the collection as a list of "name@version" strings.
func (e EncoderCollection) NameVersions() []string {
	set := strset.New()
	for _, f := range e.encoders {
		if f.Version() == sbom.AnyVersion {
			set.Add(string(f.ID()))
		} else {
			set.Add(fmt.Sprintf("%s@%s", f.ID(), f.Version()))
		}
	}

	list := set.List()
	sort.Strings(list)

	return list
}

// Aliases returns all format aliases represented in the collection (where an ID would be "spdx-tag-value" the alias would be "spdx").
func (e EncoderCollection) Aliases() []string {
	aliases := strset.New()
	for _, f := range e.encoders {
		aliases.Add(f.Aliases()...)
	}
	lst := aliases.List()
	sort.Strings(lst)
	return lst
}

// Get returns the contained encoder for a given format name and version.
func (e EncoderCollection) Get(name string, version string) sbom.FormatEncoder {
	log.WithFields("name", name, "version", version).Trace("looking for matching encoder")

	name = cleanFormatName(name)
	var mostRecentFormat sbom.FormatEncoder

	for _, f := range e.encoders {
		defaultVersion := e.defaultVersions[f.ID()]
		log.WithFields("name", f.ID(), "version", f.Version(), "aliases", f.Aliases()).Trace("considering format")
		names := []string{string(f.ID())}
		names = append(names, f.Aliases()...)
		for _, n := range names {
			if cleanFormatName(n) == name {
				// if the name or alias matches, and the requested version is any-version, use the default
				if version == sbom.AnyVersion && defaultVersion != "" {
					version = defaultVersion
				}
				if versionMatches(f.Version(), version) {
					if mostRecentFormat == nil || f.Version() > mostRecentFormat.Version() {
						mostRecentFormat = f
					}
				}
			}
		}
	}

	if mostRecentFormat != nil {
		log.WithFields("name", mostRecentFormat.ID(), "version", mostRecentFormat.Version()).Trace("found matching encoder")
	} else {
		log.WithFields("search-name", name, "search-version", version).Trace("no matching encoder found")
	}

	return mostRecentFormat
}

// GetByString accepts a name@version string, such as:
//   - json
//   - spdx-json@2.1
//   - cdx@1.5
func (e EncoderCollection) GetByString(s string) sbom.FormatEncoder {
	parts := strings.SplitN(s, "@", 2)
	version := sbom.AnyVersion
	if len(parts) > 1 {
		version = parts[1]
	}
	return e.Get(parts[0], version)
}

func versionMatches(version string, match string) bool {
	if version == sbom.AnyVersion || match == sbom.AnyVersion {
		return true
	}

	match = strings.ReplaceAll(match, ".", "\\.")
	match = strings.ReplaceAll(match, "*", ".*")
	match = fmt.Sprintf("^%s(\\..*)*$", match)
	matcher, err := regexp.Compile(match)
	if err != nil {
		return false
	}
	return matcher.MatchString(version)
}

func cleanFormatName(name string) string {
	r := strings.NewReplacer("-", "", "_", "")
	return strings.ToLower(r.Replace(name))
}

// Encode takes all SBOM elements and a format option and encodes an SBOM document.
func Encode(s sbom.SBOM, f sbom.FormatEncoder) ([]byte, error) {
	buff := bytes.Buffer{}

	if err := f.Encode(&buff, s); err != nil {
		return nil, fmt.Errorf("unable to encode sbom: %w", err)
	}

	return buff.Bytes(), nil
}

type defaultVersionIndicator interface {
	// DefaultVersion a sbom.FormatEncoder may indicate it is the default version to be used
	// when no version is specified for the type
	DefaultVersion() bool
}
