// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// 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 annotations

import (
	networking "istio.io/api/networking/v1alpha3"
)

const (
	enableCanary          = "canary"
	canaryByHeader        = "canary-by-header"
	canaryByHeaderValue   = "canary-by-header-value"
	canaryByHeaderPattern = "canary-by-header-pattern"
	canaryByCookie        = "canary-by-cookie"
	canaryWeight          = "canary-weight"
	canaryWeightTotal     = "canary-weight-total"

	defaultCanaryWeightTotal = 100
)

var _ Parser = &canary{}

type CanaryConfig struct {
	Enabled       bool
	Header        string
	HeaderValue   string
	HeaderPattern string
	Cookie        string
	Weight        int
	WeightTotal   int
}

type canary struct{}

func (c canary) Parse(annotations Annotations, config *Ingress, _ *GlobalContext) error {
	if !needCanaryConfig(annotations) {
		return nil
	}

	canaryConfig := &CanaryConfig{
		WeightTotal: defaultCanaryWeightTotal,
	}

	defer func() {
		config.Canary = canaryConfig
	}()

	canaryConfig.Enabled, _ = annotations.ParseBoolASAP(enableCanary)
	if !canaryConfig.Enabled {
		return nil
	}

	if header, err := annotations.ParseStringASAP(canaryByHeader); err == nil {
		canaryConfig.Header = header
	}

	if headerValue, err := annotations.ParseStringASAP(canaryByHeaderValue); err == nil &&
		headerValue != "" {
		canaryConfig.HeaderValue = headerValue
		return nil
	}

	if headerPattern, err := annotations.ParseStringASAP(canaryByHeaderPattern); err == nil &&
		headerPattern != "" {
		canaryConfig.HeaderPattern = headerPattern
		return nil
	}

	if cookie, err := annotations.ParseStringASAP(canaryByCookie); err == nil &&
		cookie != "" {
		canaryConfig.Cookie = cookie
		return nil
	}

	canaryConfig.Weight, _ = annotations.ParseIntASAP(canaryWeight)
	if weightTotal, err := annotations.ParseIntASAP(canaryWeightTotal); err == nil && weightTotal > 0 {
		canaryConfig.WeightTotal = weightTotal
	}

	return nil
}

func ApplyByWeight(canary, route *networking.HTTPRoute, canaryIngress *Ingress) {
	if len(route.Route) == 1 {
		// Move route level to destination level
		route.Route[0].Headers = route.Headers
		route.Headers = nil
	}

	// Modify canary weighted cluster
	canary.Route[0].Weight = int32(canaryIngress.Canary.Weight)

	// Append canary weight upstream service.
	// We will process total weight in the end.
	route.Route = append(route.Route, canary.Route[0])

	// Move route level to destination level
	canary.Route[0].Headers = canary.Headers

	// First add normal route cluster
	canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
		route.Route[0].Destination.DeepCopy())
	// Second add fallback cluster of normal route cluster
	canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
		route.Route[0].FallbackClusters...)
}

func ApplyByHeader(canary, route *networking.HTTPRoute, canaryIngress *Ingress) {
	canaryConfig := canaryIngress.Canary

	// Copy canary http route
	temp := canary.DeepCopy()

	// Inherit configuration from non-canary rule
	route.DeepCopyInto(canary)
	// Assign temp copied canary route match
	canary.Match = temp.Match
	// Assign temp copied canary route destination
	canary.Route = temp.Route

	// Modified match base on by header
	if canaryConfig.Header != "" {
		canary.Match[0].Headers = map[string]*networking.StringMatch{
			canaryConfig.Header: {
				MatchType: &networking.StringMatch_Exact{
					Exact: "always",
				},
			},
		}
		if canaryConfig.HeaderValue != "" {
			canary.Match[0].Headers = map[string]*networking.StringMatch{
				canaryConfig.Header: {
					MatchType: &networking.StringMatch_Regex{
						Regex: "always|" + canaryConfig.HeaderValue,
					},
				},
			}
		} else if canaryConfig.HeaderPattern != "" {
			canary.Match[0].Headers = map[string]*networking.StringMatch{
				canaryConfig.Header: {
					MatchType: &networking.StringMatch_Regex{
						Regex: canaryConfig.HeaderPattern,
					},
				},
			}
		}
	} else if canaryConfig.Cookie != "" {
		canary.Match[0].Headers = map[string]*networking.StringMatch{
			"cookie": {
				MatchType: &networking.StringMatch_Regex{
					Regex: "^(.\\*?;)?(" + canaryConfig.Cookie + "=always)(;.\\*)?$",
				},
			},
		}
	}

	// First add normal route cluster
	canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
		route.Route[0].Destination.DeepCopy())
	// Second add fallback cluster of normal route cluster
	canary.Route[0].FallbackClusters = append(canary.Route[0].FallbackClusters,
		route.Route[0].FallbackClusters...)
}

func needCanaryConfig(annotations Annotations) bool {
	return annotations.HasASAP(enableCanary)
}
