// Copyright Istio 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 model

import (
	"regexp"
	"strings"
	"time"

	"google.golang.org/protobuf/proto"
	"k8s.io/apimachinery/pkg/types"

	networking "istio.io/api/networking/v1alpha3"
	"istio.io/api/type/v1beta1"
	"istio.io/istio/pkg/config"
	"istio.io/istio/pkg/config/host"
	"istio.io/istio/pkg/config/labels"
	"istio.io/istio/pkg/config/xds"
	"istio.io/istio/pkg/util/sets"
)

// EnvoyFilterWrapper is a wrapper for the EnvoyFilter api object with pre-processed data
type EnvoyFilterWrapper struct {
	Name                         string
	Namespace                    string
	ReferencedNamespacedServices sets.Set[NamespacedHostname]
	ReferencedServices           sets.String
	workloadSelector             labels.Instance
	Patches                      map[networking.EnvoyFilter_ApplyTo][]*EnvoyFilterConfigPatchWrapper
	Priority                     int32
	creationTime                 time.Time

	fullSpec *networking.EnvoyFilter
}

func (efw *EnvoyFilterWrapper) NamespacedName() types.NamespacedName {
	return types.NamespacedName{
		Name:      efw.Name,
		Namespace: efw.Namespace,
	}
}

var _ TargetablePolicy = &EnvoyFilterWrapper{}

func (efw *EnvoyFilterWrapper) GetTargetRef() *v1beta1.PolicyTargetReference {
	// This API never had this legacy field
	return nil
}

func (efw *EnvoyFilterWrapper) GetTargetRefs() []*v1beta1.PolicyTargetReference {
	if efw.fullSpec == nil {
		return nil
	}
	return efw.fullSpec.TargetRefs
}

func (efw *EnvoyFilterWrapper) GetSelector() *v1beta1.WorkloadSelector {
	if efw.workloadSelector != nil {
		return &v1beta1.WorkloadSelector{
			MatchLabels: efw.workloadSelector,
		}
	}
	return nil
}

// maxProxyVersionLen bounds the proxyVersion match expression length. Version strings are short;
// very long patterns are expensive to compile and are treated as a non-match rather than compiled.
// Kept in sync with the same const in pkg/config/validation/envoyfilter, which warns on it.
const maxProxyVersionLen = 1024

// EnvoyFilterConfigPatchWrapper is a wrapper over the EnvoyFilter ConfigPatch api object
// fields are ordered such that this struct is aligned
type EnvoyFilterConfigPatchWrapper struct {
	Value     proto.Message
	Match     *networking.EnvoyFilter_EnvoyConfigObjectMatch
	ApplyTo   networking.EnvoyFilter_ApplyTo
	Operation networking.EnvoyFilter_Patch_Operation
	// Pre-compile the regex from proxy version match in the match
	ProxyVersionRegex *regexp.Regexp
	// ProxyPrefixMatch provides a prefix match for the proxy version. The current API only allows
	// regex match, but as an optimization we can reduce this to a prefix match for common cases.
	// If this is set, ProxyVersionRegex is ignored.
	ProxyPrefixMatch string
	Name             string
	Namespace        string
	FullName         string
	// ProxyVersionNoMatch is set when the proxyVersion match expression could not be safely compiled
	// (e.g. it exceeds maxProxyVersionLen). When true the patch matches no proxy (fail closed) rather
	// than matching every version, which is what a nil ProxyVersionRegex would otherwise do.
	ProxyVersionNoMatch bool
}

// wellKnownVersions defines a mapping of well known regex matches to prefix matches
// This is done only as an optimization; behavior should remain the same
// All versions specified by the default installation (Telemetry V2) should be added here.
var wellKnownVersions = map[string]string{
	`^1\.16.*`: "1.16",
	`^1\.17.*`: "1.17",
	`^1\.18.*`: "1.18",
	`^1\.19.*`: "1.19",
	`^1\.20.*`: "1.20",
	`^1\.21.*`: "1.21",
	`^1\.22.*`: "1.22",
	`^1\.23.*`: "1.23",
	// Hopefully we have a better API by 1.23. If not, add it here
}

// convertToEnvoyFilterWrapper converts from EnvoyFilter config to EnvoyFilterWrapper object
func convertToEnvoyFilterWrapper(local *config.Config) *EnvoyFilterWrapper {
	localEnvoyFilter := local.Spec.(*networking.EnvoyFilter)

	out := &EnvoyFilterWrapper{
		Name:                         local.Name,
		Namespace:                    local.Namespace,
		ReferencedNamespacedServices: nil,
		ReferencedServices:           nil,
		workloadSelector:             nil,
		Patches:                      nil,
		Priority:                     localEnvoyFilter.Priority,
		creationTime:                 local.CreationTimestamp,
		fullSpec:                     localEnvoyFilter,
	}
	if localEnvoyFilter.WorkloadSelector != nil {
		out.workloadSelector = localEnvoyFilter.WorkloadSelector.Labels
	}

	// For Waypoint/Gateway with filtering, we need to explicitly opt-in to included services.
	// This annotation allows doing so.
	// Note: sidecar use cases should make sure to use Sidecar to include them; this will not override exclusions by sidecar
	svcRefs := local.Annotations["envoyfilter.istio.io/referenced-services"]
	for _, ref := range strings.Split(svcRefs, ",") {
		ns, h, ok := strings.Cut(ref, "/")
		if ok {
			if out.ReferencedNamespacedServices == nil {
				out.ReferencedNamespacedServices = sets.New[NamespacedHostname]()
			}
			out.ReferencedNamespacedServices.Insert(NamespacedHostname{Hostname: host.Name(h), Namespace: ns})
		} else {
			if out.ReferencedServices == nil {
				out.ReferencedServices = sets.New[string]()
			}
			out.ReferencedServices.Insert(ref)
		}
	}
	out.Patches = make(map[networking.EnvoyFilter_ApplyTo][]*EnvoyFilterConfigPatchWrapper)
	for _, cp := range localEnvoyFilter.ConfigPatches {
		if cp.Patch == nil {
			// Should be caught by validation, but sometimes its disabled and we don't want to crash
			// as a result.
			log.Debugf("envoyfilter %s/%s discarded due to missing patch", local.Namespace, local.Name)
			continue
		}
		cpw := &EnvoyFilterConfigPatchWrapper{
			Name:      local.Name,
			Namespace: local.Namespace,
			FullName:  local.Namespace + "/" + local.Name,
			ApplyTo:   cp.ApplyTo,
			Match:     cp.Match,
			Operation: cp.Patch.Operation,
		}
		var err error
		// Use non-strict building to avoid issues where EnvoyFilter is valid but meant
		// for a different version of the API than we are built with
		cpw.Value, err = xds.BuildXDSObjectFromStruct(cp.ApplyTo, cp.Patch.Value, false)
		// There generally won't be an error here because validation catches mismatched types
		// Should only happen in tests or without validation
		if err != nil {
			log.Errorf("envoyfilter %s/%s failed to build envoy filter value: %+v", local.Namespace, local.Name, err)
			continue
		}
		if cp.Match == nil {
			// create a match all object
			cpw.Match = &networking.EnvoyFilter_EnvoyConfigObjectMatch{Context: networking.EnvoyFilter_ANY}
		} else if cp.Match.Proxy != nil && cp.Match.Proxy.ProxyVersion != "" {
			// Attempt to convert regex to a simple prefix match for the common case of matching
			// a standard Istio version. This field should likely be replaced with semver, but for now
			// we can workaround the performance impact of regex
			if prefix, f := wellKnownVersions[cp.Match.Proxy.ProxyVersion]; f {
				cpw.ProxyPrefixMatch = prefix
			} else if len(cp.Match.Proxy.ProxyVersion) > maxProxyVersionLen {
				// Don't compile pathologically long patterns; the compile is expensive enough to be a
				// DoS vector against istiod. Fail closed so the patch matches no proxy.
				cpw.ProxyVersionNoMatch = true
			} else {
				// pre-compile the regex for proxy version if it exists
				// ignore the error because validation catches invalid regular expressions.
				cpw.ProxyVersionRegex, _ = regexp.Compile(cp.Match.Proxy.ProxyVersion)
			}
		}

		if _, exists := out.Patches[cp.ApplyTo]; !exists {
			out.Patches[cp.ApplyTo] = make([]*EnvoyFilterConfigPatchWrapper, 0)
		}
		if cpw.Operation == networking.EnvoyFilter_Patch_INSERT_AFTER ||
			cpw.Operation == networking.EnvoyFilter_Patch_INSERT_BEFORE ||
			cpw.Operation == networking.EnvoyFilter_Patch_INSERT_FIRST {
			// insert_before, after or first is applicable for listener filter, network filter,
			// http filter and http route, convert the rest to add
			if cpw.ApplyTo != networking.EnvoyFilter_HTTP_FILTER &&
				cpw.ApplyTo != networking.EnvoyFilter_NETWORK_FILTER &&
				cpw.ApplyTo != networking.EnvoyFilter_HTTP_ROUTE &&
				cpw.ApplyTo != networking.EnvoyFilter_LISTENER_FILTER {
				cpw.Operation = networking.EnvoyFilter_Patch_ADD
			}
		}
		out.Patches[cp.ApplyTo] = append(out.Patches[cp.ApplyTo], cpw)
	}
	return out
}

func proxyMatch(proxy *Proxy, cp *EnvoyFilterConfigPatchWrapper) bool {
	if cp.Match.Proxy == nil {
		return true
	}

	if cp.ProxyVersionNoMatch {
		// the proxyVersion match could not be safely compiled, so we can't evaluate it; fail closed
		return false
	}

	if cp.ProxyPrefixMatch != "" {
		if !strings.HasPrefix(proxy.Metadata.IstioVersion, cp.ProxyPrefixMatch) {
			return false
		}
	}
	if cp.ProxyVersionRegex != nil {
		ver := proxy.Metadata.IstioVersion
		if ver == "" {
			// we do not have a proxy version but the user has a regex. so this is a mismatch
			return false
		}
		if !cp.ProxyVersionRegex.MatchString(ver) {
			return false
		}
	}

	for k, v := range cp.Match.Proxy.Metadata {
		if proxy.Metadata.Raw[k] != v {
			return false
		}
	}
	return true
}

// Returns the keys of all the wrapped envoyfilters.
func (efw *EnvoyFilterWrapper) Keys() []string {
	if efw == nil {
		return nil
	}
	keys := sets.String{}
	for _, patches := range efw.Patches {
		for _, patch := range patches {
			keys.Insert(patch.Key())
		}
	}
	return sets.SortedList(keys)
}

// Returns the keys of all the wrapped envoyfilters.
func (efw *MergedEnvoyFilterWrapper) KeysApplyingTo(applyTo ...networking.EnvoyFilter_ApplyTo) []string {
	if efw == nil {
		return nil
	}
	keys := sets.String{}
	for _, a := range applyTo {
		for _, patch := range efw.Patches[a] {
			keys.Insert(patch.Key())
		}
	}
	return sets.SortedList(keys)
}

func (cpw *EnvoyFilterConfigPatchWrapper) Key() string {
	if cpw == nil {
		return ""
	}
	return cpw.FullName
}
