// Copyright 2025 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package metadataapi

import (
	"fmt"
	"iter"
	"reflect"
	"slices"
	"strings"

	"github.com/blang/semver/v4"

	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apimachinery/pkg/util/validation/field"

	azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
)

const (
	OpenShiftVersionPrefix = "openshift-v"
)

// AllowedMajorUpgrades is for OpenShift cross-major upgrades (one major at a time, e.g. 4 to 5). Keys and values
// are major.minor lines ("x.y"): the key is the current version's line, the value is the only allowed line for
// the desired version. Used when validating version on clusters and node pools.
// Add entries when new cross-major paths are supported.
var AllowMajorUpgradePaths = map[string]string{
	"4.22": "5.0",
	"4.23": "5.1",
}

// AllowControlPlaneNodePoolMajorVersionSkew maps node pool OpenShift minor release lines (x.y) to allowed
// control-plane minor release lines when the node pool major differs from the cluster major. Values are
// sorted from strictest to most permissive allowed skew. See the HyperShift control plane version status
// enhancement for node pool skews against cluster version:
// https://github.com/openshift/enhancements/blob/master/enhancements/hypershift/hypershift-control-plane-version-status.md
var AllowControlPlaneNodePoolMajorVersionSkew = map[string][]string{
	"4.21": {"5.0"},
	"4.22": {"5.0", "5.1"},
	"4.23": {"5.1", "5.2"},
}

// AllowedChannelGroups is the set ARO-HCP allows to use for customer purposes
var AllowedChannelGroups = sets.New("stable", "fast")

// AllowedChannelGroupsWithExperimentalFlag is the set the service allows to use when using the Experimental Feature AFEC flag
var AllowedChannelGroupsWithExperimentalFlag = sets.New("stable", "fast", "candidate", "nightly")

// NextMinorReleaseLine returns the next OpenShift minor release line after v as
// major.minor.0 (patch and pre-release metadata cleared).
//
// Within a major, that is major.(minor+1).0. At major boundaries that do not
// continue as major.(minor+1), a bridged next-major line is returned instead
// (for example 4.23.x → 5.1.0).
func NextMinorReleaseLine(v semver.Version) semver.Version {
	if targetReleaseLine, ok := AllowMajorUpgradePaths[fmt.Sprintf("%d.%d", v.Major, v.Minor)]; ok {
		return Must(semver.ParseTolerant(targetReleaseLine))
	}
	return semver.Version{Major: v.Major, Minor: v.Minor + 1}
}

// Ptr returns a pointer to p.
func Ptr[T any](p T) *T {
	return &p
}

// Deref returns the value pointed to by p, or the zero value if p is nil.
func Deref[T any](p *T) T {
	if p == nil {
		var zero T
		return zero
	}
	return *p
}

// Copied from Go's src/encoding/json/encode.go
func isEmptyValue(v reflect.Value) bool {
	switch v.Kind() {
	case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
		return v.Len() == 0
	case reflect.Bool,
		reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
		reflect.Float32, reflect.Float64,
		reflect.Interface, reflect.Pointer, reflect.Struct:
		return v.IsZero()
	}
	return false
}

func ResourceIDToStringPtr(resourceID *azcorearm.ResourceID) *string {
	if resourceID == nil {
		return nil
	}
	return Ptr(resourceID.String())
}

// PtrOrNil returns a pointer to p or nil if p is an empty value as
// would be determined by the "omitempty" option in json.Marshal.
func PtrOrNil[T any](p T) *T {
	if isEmptyValue(reflect.ValueOf(p)) {
		return nil
	}
	return &p
}

// TrimStringSlice returns a new string slice with all leading and
// trailing white space removed from each element and omitting any
// white-space-only elements.
func TrimStringSlice(s []string) []string {
	// Preserve nil in case it matters.
	if s == nil {
		return nil
	}
	out := make([]string, 0, len(s))
	for _, item := range s {
		item = strings.TrimSpace(item)
		if len(item) > 0 {
			out = append(out, item)
		}
	}
	return out
}

// DeleteNilsFromPtrSlice returns a slice with nil pointers removed.
func DeleteNilsFromPtrSlice[S ~[]*E, E any](s S) S {
	return slices.DeleteFunc(s, func(e *E) bool { return e == nil })
}

// NonNilSliceValues returns an iterator over index-value pairs in a slice
// of pointers in the usual order, but skipping over nils.
func NonNilSliceValues[E any](a []*E) iter.Seq2[int, *E] {
	return NonNilValues(slices.All(a))
}

// NonNilValues returns an iterator over a sequence of pairs of values that skips
// pairs where the second value in the pair is nil.
func NonNilValues[K any, V any](seq iter.Seq2[K, *V]) iter.Seq2[K, *V] {
	return func(yield func(K, *V) bool) {
		for k, v := range seq {
			if v != nil {
				if !yield(k, v) {
					return
				}
			}
		}
	}
}

// StringSliceToStringPtrSlice converts a slice of strings to a slice of string pointers.
func StringSliceToStringPtrSlice(s []string) []*string {
	// Preserve nil in case it matters.
	if s == nil {
		return nil
	}
	out := make([]*string, len(s))
	for index, item := range s {
		out[index] = Ptr(item)
	}
	return out
}

// StringPtrSliceToStringSlice converts a slice of string pointers to a slice of strings.
func StringPtrSliceToStringSlice(s []*string) []string {
	// Preserve nil in case it matters.
	if s == nil {
		return nil
	}
	s = DeleteNilsFromPtrSlice(s)
	out := make([]string, 0, len(s))
	for _, item := range s {
		out = append(out, *item)
	}
	return out
}

func ResourceIDMapToStringPtrMap(m map[string]*azcorearm.ResourceID) map[string]*string {
	// Preserve nil in case it matters.
	if m == nil {
		return nil
	}
	out := make(map[string]*string, len(m))
	for key, val := range m {
		out[key] = ResourceIDToStringPtr(val)
	}
	return out
}

// StringMapToStringPtrMap converts a map of strings to a map of string pointers.
func StringMapToStringPtrMap(m map[string]string) map[string]*string {
	// Preserve nil in case it matters.
	if m == nil {
		return nil
	}
	out := make(map[string]*string, len(m))
	for key, val := range m {
		out[key] = Ptr(val)
	}
	return out
}

// StringPtrMapToStringMap converts a map of string pointers to a map of strings.
func StringPtrMapToStringMap(m map[string]*string) map[string]string {
	// Preserve nil in case it matters.
	if m == nil {
		return nil
	}
	out := make(map[string]string, len(m))
	for key, val := range m {
		if val != nil {
			out[key] = *val
		}
	}
	return out
}

// MergeStringPtrMap merges a map of string pointers into a map of strings
// following the rules of JSON merge-patch (RFC 7396). In particular, if a
// key in src has a nil value, that entry is deleted from dst. The function
// takes a pointer to the dst map in case the dst map is nil and needs to be
// initialized.
func MergeStringPtrMapIntoResourceIDMap(fldPath *field.Path, src map[string]*string, dst *map[string]*azcorearm.ResourceID) field.ErrorList {
	errs := field.ErrorList{}

	if src != nil && dst != nil {
		// convert in order so errors are in order.
		for _, key := range sets.StringKeySet(src).List() {
			val := src[key]
			if val == nil {
				delete(*dst, key)
			} else {
				if *dst == nil {
					*dst = make(map[string]*azcorearm.ResourceID)
				}
				if len(*val) > 0 {
					if resourceID, err := azcorearm.ParseResourceID(*val); err != nil {
						errs = append(errs, field.Invalid(fldPath.Key(key), *val, err.Error()))
					} else {
						(*dst)[key] = resourceID
					}
				}
			}
		}
	}

	return errs
}
