/*
Copyright 2023 The Kubernetes Authors.

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 environment

import (
	"fmt"
	"strconv"
	"sync"

	"github.com/google/cel-go/cel"
	"github.com/google/cel-go/ext"
	"golang.org/x/sync/singleflight"

	"k8s.io/apimachinery/pkg/util/version"
	celconfig "k8s.io/apiserver/pkg/apis/cel"
	"k8s.io/apiserver/pkg/cel/library"
)

// DefaultCompatibilityVersion returns a default compatibility version for use with EnvSet
// that guarantees compatibility with CEL features/libraries/parameters understood by
// an n-1 version
//
// This default will be set to no more than n-1 the current Kubernetes major.minor version.
//
// Note that a default version number less than n-1 indicates a wider range of version
// compatibility than strictly required for rollback. A wide range of compatibility is
// desirable because it means that CEL expressions are portable across a wider range
// of Kubernetes versions.
func DefaultCompatibilityVersion() *version.Version {
	return version.MajorMinor(1, 27)
}

var baseOpts = []VersionedOptions{
	{
		// CEL epoch was actually 1.23, but we artificially set it to 1.0 because these
		// options should always be present.
		IntroducedVersion: version.MajorMinor(1, 0),
		EnvOptions: []cel.EnvOption{
			cel.HomogeneousAggregateLiterals(),
			// Validate function declarations once during base env initialization,
			// so they don't need to be evaluated each time a CEL rule is compiled.
			// This is a relatively expensive operation.
			cel.EagerlyValidateDeclarations(true),
			cel.DefaultUTCTimeZone(true),

			ext.Strings(ext.StringsVersion(0)),
			library.URLs(),
			library.Regex(),
			library.Lists(),
		},
		ProgramOptions: []cel.ProgramOption{
			cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost),
			cel.CostLimit(celconfig.PerCallLimit),
		},
	},
	{
		IntroducedVersion: version.MajorMinor(1, 27),
		EnvOptions: []cel.EnvOption{
			library.Authz(),
		},
	},
	{
		IntroducedVersion: version.MajorMinor(1, 28),
		EnvOptions: []cel.EnvOption{
			cel.CrossTypeNumericComparisons(true),
			cel.OptionalTypes(),
			library.Quantity(),
		},
	},
	// TODO: switch to ext.Strings version 2 once format() is fixed to work with HomogeneousAggregateLiterals.
}

// MustBaseEnvSet returns the common CEL base environments for Kubernetes for Version, or panics
// if the version is nil, or does not have major and minor components.
//
// The returned environment contains function libraries, language settings, optimizations and
// runtime cost limits appropriate CEL as it is used in Kubernetes.
//
// The returned environment contains no CEL variable definitions or custom type declarations and
// should be extended to construct environments with the appropriate variable definitions,
// type declarations and any other needed configuration.
func MustBaseEnvSet(ver *version.Version) *EnvSet {
	if ver == nil {
		panic("version must be non-nil")
	}
	if len(ver.Components()) < 2 {
		panic(fmt.Sprintf("version must contain an major and minor component, but got: %s", ver.String()))
	}
	key := strconv.FormatUint(uint64(ver.Major()), 10) + "." + strconv.FormatUint(uint64(ver.Minor()), 10)
	if entry, ok := baseEnvs.Load(key); ok {
		return entry.(*EnvSet)
	}

	entry, _, _ := baseEnvsSingleflight.Do(key, func() (interface{}, error) {
		entry := mustNewEnvSet(ver, baseOpts)
		baseEnvs.Store(key, entry)
		return entry, nil
	})
	return entry.(*EnvSet)
}

var (
	baseEnvs             = sync.Map{}
	baseEnvsSingleflight = &singleflight.Group{}
)
