package image

import (
	"fmt"
	"regexp"
	"runtime"
	"strings"

	"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/common"
	"github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options"
)

// DEPRECATED: This function has been removed in the CRD branch and will be deprecated and removed in a future release.
// The CRD branch introduces a new architecture that eliminates the need for this annotation-based approach.
// GetParameterHelmImageName gets the value for image-name option for the image
// from a set of annotations
func (img *ContainerImage) GetParameterHelmImageName(annotations map[string]string, annotationPrefix string) string {
	key := fmt.Sprintf(common.Prefixed(annotationPrefix, common.HelmParamImageNameAnnotationSuffix), img.normalizedSymbolicName())
	val, ok := annotations[key]
	if !ok {
		return ""
	}
	return val
}

// DEPRECATED: This function has been removed in the CRD branch and will be deprecated and removed in a future release.
// The CRD branch introduces a new architecture that eliminates the need for this annotation-based approach.
// GetParameterHelmImageTag gets the value for image-tag option for the image
// from a set of annotations
func (img *ContainerImage) GetParameterHelmImageTag(annotations map[string]string, annotationPrefix string) string {
	key := fmt.Sprintf(common.Prefixed(annotationPrefix, common.HelmParamImageTagAnnotationSuffix), img.normalizedSymbolicName())
	val, ok := annotations[key]
	if !ok {
		return ""
	}
	return val
}

// DEPRECATED: This function has been removed in the CRD branch and will be deprecated and removed in a future release.
// The CRD branch introduces a new architecture that eliminates the need for this annotation-based approach.
// GetParameterHelmImageSpec gets the value for image-spec option for the image
// from a set of annotations
func (img *ContainerImage) GetParameterHelmImageSpec(annotations map[string]string, annotationPrefix string) string {
	key := fmt.Sprintf(common.Prefixed(annotationPrefix, common.HelmParamImageSpecAnnotationSuffix), img.normalizedSymbolicName())
	val, ok := annotations[key]
	if !ok {
		return ""
	}
	return val
}

// DEPRECATED: This function has been removed in the CRD branch and will be deprecated and removed in a future release.
// The CRD branch introduces a new architecture that eliminates the need for this annotation-based approach.
// GetParameterKustomizeImageName gets the value for image-spec option for the
// image from a set of annotations
func (img *ContainerImage) GetParameterKustomizeImageName(annotations map[string]string, annotationPrefix string) string {
	key := fmt.Sprintf(common.Prefixed(annotationPrefix, common.KustomizeApplicationNameAnnotationSuffix), img.normalizedSymbolicName())
	val, ok := annotations[key]
	if !ok {
		return ""
	}
	return val
}

// DEPRECATED: This function has been removed in the CRD branch and will be deprecated and removed in a future release.
// The CRD branch introduces a new architecture that eliminates the need for this annotation-based approach.
// HasForceUpdateOptionAnnotation gets the value for force-update option for the
// image from a set of annotations
func (img *ContainerImage) HasForceUpdateOptionAnnotation(annotations map[string]string, annotationPrefix string) bool {
	forceUpdateAnnotations := []string{
		fmt.Sprintf(common.Prefixed(annotationPrefix, common.ForceUpdateOptionAnnotationSuffix), img.normalizedSymbolicName()),
		common.Prefixed(annotationPrefix, common.ApplicationWideForceUpdateOptionAnnotationSuffix),
	}
	var forceUpdateVal = ""
	for _, key := range forceUpdateAnnotations {
		if val, ok := annotations[key]; ok {
			forceUpdateVal = val
			break
		}
	}
	return forceUpdateVal == "true"
}

// DEPRECATED: This function has been removed in the CRD branch and will be deprecated and removed in a future release.
// The CRD branch introduces a new architecture that eliminates the need for this annotation-based approach.
// GetParameterSort gets and validates the value for the sort option for the
// image from a set of annotations
func (img *ContainerImage) GetParameterUpdateStrategy(annotations map[string]string, annotationPrefix string) UpdateStrategy {
	updateStrategyAnnotations := []string{
		fmt.Sprintf(common.Prefixed(annotationPrefix, common.UpdateStrategyAnnotationSuffix), img.normalizedSymbolicName()),
		common.Prefixed(annotationPrefix, common.ApplicationWideUpdateStrategyAnnotationSuffix),
	}
	var updateStrategyVal = ""
	for _, key := range updateStrategyAnnotations {
		if val, ok := annotations[key]; ok {
			updateStrategyVal = val
			break
		}
	}
	logCtx := img.LogContext()
	if updateStrategyVal == "" {
		logCtx.Tracef("No sort option found")
		// Default is sort by version
		return StrategySemVer
	}
	logCtx.Tracef("Found update strategy %s", updateStrategyVal)
	return img.ParseUpdateStrategy(updateStrategyVal)
}

func (img *ContainerImage) ParseUpdateStrategy(val string) UpdateStrategy {
	logCtx := img.LogContext()
	switch strings.ToLower(val) {
	case "semver":
		return StrategySemVer
	case "latest":
		logCtx.Warnf("\"latest\" strategy has been renamed to \"newest-build\". Please switch to the new convention as support for the old naming convention will be removed in future versions.")
		fallthrough
	case "newest-build":
		return StrategyNewestBuild
	case "name":
		logCtx.Warnf("\"name\" strategy has been renamed to \"alphabetical\". Please switch to the new convention as support for the old naming convention will be removed in future versions.")
		fallthrough
	case "alphabetical":
		return StrategyAlphabetical
	case "digest":
		return StrategyDigest
	default:
		logCtx.Warnf("Unknown sort option %s -- using semver", val)
		return StrategySemVer
	}
}

// DEPRECATED: This function has been removed in the CRD branch and will be deprecated and removed in a future release.
// The CRD branch introduces a new architecture that eliminates the need for this annotation-based approach.
// GetParameterMatch returns the match function and pattern to use for matching
// tag names. If an invalid option is found, it returns MatchFuncNone as the
// default, to prevent accidental matches.
func (img *ContainerImage) GetParameterMatch(annotations map[string]string, annotationPrefix string) (MatchFuncFn, interface{}) {
	allowTagsAnnotations := []string{
		fmt.Sprintf(common.Prefixed(annotationPrefix, common.AllowTagsOptionAnnotationSuffix), img.normalizedSymbolicName()),
		common.Prefixed(annotationPrefix, common.ApplicationWideAllowTagsOptionAnnotationSuffix),
	}
	var allowTagsVal = ""
	for _, key := range allowTagsAnnotations {
		if val, ok := annotations[key]; ok {
			allowTagsVal = val
			break
		}
	}
	logCtx := img.LogContext()
	if allowTagsVal == "" {
		// The old match-tag annotation is deprecated and will be subject to removal
		// in a future version.
		key := fmt.Sprintf(common.Prefixed(annotationPrefix, common.OldMatchOptionAnnotationSuffix), img.normalizedSymbolicName())
		val, ok := annotations[key]
		if ok {
			logCtx.Warnf("The 'tag-match' annotation is deprecated and subject to removal. Please use 'allow-tags' annotation instead.")
			allowTagsVal = val
		}
	}
	if allowTagsVal == "" {
		logCtx.Tracef("No match annotation found")
		return MatchFuncAny, ""
	}
	return img.ParseMatchfunc(allowTagsVal)
}

// ParseMatchfunc returns a matcher function and its argument from given value
func (img *ContainerImage) ParseMatchfunc(val string) (MatchFuncFn, interface{}) {
	logCtx := img.LogContext()

	// The special value "any" doesn't take any parameter
	if strings.ToLower(val) == "any" {
		return MatchFuncAny, nil
	}

	opt := strings.SplitN(val, ":", 2)
	if len(opt) != 2 {
		logCtx.Warnf("Invalid match option syntax '%s', ignoring", val)
		return MatchFuncNone, nil
	}
	switch strings.ToLower(opt[0]) {
	case "regexp":
		re, err := regexp.Compile(opt[1])
		if err != nil {
			logCtx.Warnf("Could not compile regexp '%s'", opt[1])
			return MatchFuncNone, nil
		}
		return MatchFuncRegexp, re
	default:
		logCtx.Warnf("Unknown match function: %s", opt[0])
		return MatchFuncNone, nil
	}
}

// DEPRECATED: This function has been removed in the CRD branch and will be deprecated and removed in a future release.
// The CRD branch introduces a new architecture that eliminates the need for this annotation-based approach.
// GetParameterPullSecret retrieves an image's pull secret credentials
func (img *ContainerImage) GetParameterPullSecret(annotations map[string]string, annotationPrefix string) *CredentialSource {
	pullSecretAnnotations := []string{
		fmt.Sprintf(common.Prefixed(annotationPrefix, common.PullSecretAnnotationSuffix), img.normalizedSymbolicName()),
		common.Prefixed(annotationPrefix, common.ApplicationWidePullSecretAnnotationSuffix),
	}
	var pullSecretVal = ""
	for _, key := range pullSecretAnnotations {
		if val, ok := annotations[key]; ok {
			pullSecretVal = val
			break
		}
	}
	logCtx := img.LogContext()
	if pullSecretVal == "" {
		logCtx.Tracef("No pull-secret annotation found")
		return nil
	}
	credSrc, err := ParseCredentialSource(pullSecretVal, false)
	if err != nil {
		logCtx.Warnf("Invalid credential reference specified: %s", pullSecretVal)
		return nil
	}
	return credSrc
}

// DEPRECATED: This function has been removed in the CRD branch and will be deprecated and removed in a future release.
// The CRD branch introduces a new architecture that eliminates the need for this annotation-based approach.
// GetParameterIgnoreTags retrieves a list of tags to ignore from a comma-separated string
func (img *ContainerImage) GetParameterIgnoreTags(annotations map[string]string, annotationPrefix string) []string {
	ignoreTagsAnnotations := []string{
		fmt.Sprintf(common.Prefixed(annotationPrefix, common.IgnoreTagsOptionAnnotationSuffix), img.normalizedSymbolicName()),
		common.Prefixed(annotationPrefix, common.ApplicationWideIgnoreTagsOptionAnnotationSuffix),
	}
	var ignoreTagsVal = ""
	for _, key := range ignoreTagsAnnotations {
		if val, ok := annotations[key]; ok {
			ignoreTagsVal = val
			break
		}
	}
	logCtx := img.LogContext()
	if ignoreTagsVal == "" {
		logCtx.Tracef("No ignore-tags annotation found")
		return nil
	}
	ignoreList := make([]string, 0)
	tags := strings.Split(strings.TrimSpace(ignoreTagsVal), ",")
	for _, tag := range tags {
		// We ignore empty tags
		trimmed := strings.TrimSpace(tag)
		if trimmed != "" {
			ignoreList = append(ignoreList, trimmed)
		}
	}
	return ignoreList
}

// GetPlatformOptions sets up platform constraints for an image. If no platform
// is specified in the annotations, we restrict the platform for images to the
// platform we're executed on unless unrestricted is set to true, in which case
// we do not setup a platform restriction if no platform annotation is found.
func (img *ContainerImage) GetPlatformOptions(annotations map[string]string, unrestricted bool, annotationPrefix string) *options.ManifestOptions {
	logCtx := img.LogContext()
	var opts *options.ManifestOptions = options.NewManifestOptions()
	key := fmt.Sprintf(common.Prefixed(annotationPrefix, common.PlatformsAnnotationSuffix), img.normalizedSymbolicName())
	val, ok := annotations[key]
	if !ok {
		if !unrestricted {
			os := runtime.GOOS
			arch := runtime.GOARCH
			variant := ""
			if strings.Contains(runtime.GOARCH, "/") {
				a := strings.SplitN(runtime.GOARCH, "/", 2)
				arch = a[0]
				variant = a[1]
			}
			logCtx.Tracef("Using runtime platform constraint %s", options.PlatformKey(os, arch, variant))
			opts = opts.WithPlatform(os, arch, variant)
		}
	} else {
		platforms := strings.Split(val, ",")
		for _, ps := range platforms {
			pt := strings.TrimSpace(ps)
			os, arch, variant, err := ParsePlatform(pt)
			if err != nil {
				// If the platform identifier could not be parsed, we set the
				// constraint intentionally to the invalid value so we don't
				// end up updating to the wrong architecture possibly.
				os = ps
				logCtx.Warnf("could not parse platform identifier '%v': invalid format", pt)
			}
			logCtx.Tracef("Adding platform constraint %s", options.PlatformKey(os, arch, variant))
			opts = opts.WithPlatform(os, arch, variant)
		}
	}

	return opts
}

func ParsePlatform(platformID string) (string, string, string, error) {
	p := strings.SplitN(platformID, "/", 3)
	if len(p) < 2 {
		return "", "", "", fmt.Errorf("could not parse platform constraint '%s'", platformID)
	}
	os := p[0]
	arch := p[1]
	variant := ""
	if len(p) == 3 {
		variant = p[2]
	}
	return os, arch, variant, nil
}

func (img *ContainerImage) normalizedSymbolicName() string {
	return strings.ReplaceAll(img.ImageAlias, "/", "_")
}
