package http

import (
	envv1alpha2 "alibaba.com/virtual-env-operator/pkg/apis/env/v1alpha2"
	"alibaba.com/virtual-env-operator/pkg/shared"
	"alibaba.com/virtual-env-operator/pkg/shared/logger"
	"encoding/json"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"knative.dev/pkg/apis/istio/common/v1alpha1"
	networkingv1alpha3 "knative.dev/pkg/apis/istio/v1alpha3"
	"reflect"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"strings"
)

const (
	defaultGateway = "mesh"
)

// generate istio virtual service instance
func VirtualService(namespace string, svcName string, availableLabels []string, relatedDeployments []string,
	spec envv1alpha2.VirtualEnvironmentSpec) *networkingv1alpha3.VirtualService {
	envHeaderName := spec.EnvHeader.Name
	envHeaderAliases := spec.EnvHeader.Aliases
	envSplitter := spec.EnvLabel.Splitter
	defaultSubset := spec.EnvLabel.DefaultSubset
	virtualSvc := &networkingv1alpha3.VirtualService{
		ObjectMeta: metav1.ObjectMeta{
			Name:      svcName,
			Namespace: namespace,
		},
		Spec: networkingv1alpha3.VirtualServiceSpec{
			Gateways: []string{defaultGateway},
			Hosts:    []string{svcName},
			HTTP:     []networkingv1alpha3.HTTPRoute{},
		},
	}
	serviceInfo := shared.AvailableServices[svcName]
	if len(serviceInfo.Gateways) > 0 {
		virtualSvc.Spec.Gateways = append(virtualSvc.Spec.Gateways, serviceInfo.Gateways...)
	}
	if len(serviceInfo.Hosts) > 0 {
		virtualSvc.Spec.Hosts = append(virtualSvc.Spec.Hosts, serviceInfo.Hosts...)
	}
	var httpRoute *networkingv1alpha3.HTTPRoute = nil
	if serviceInfo.CustomRule != "" {
		// This is a workaround:
		// knative sdk and istio sdk has different 'uri' field naming
		customRule := strings.ReplaceAll(serviceInfo.CustomRule, "\"Uri\"", "\"URI\"")
		if err := json.Unmarshal([]byte(customRule), &httpRoute); err == nil {
			logger.Info("Unmarshal route rule successful")
		} else {
			logger.Error(err, "Failed to unmarshal route rule annotation")
		}
	}
	for name, port := range serviceInfo.Ports {
		if !strings.HasPrefix(name, "http") {
			// skip non-http ports
			continue
		}
		for _, label := range availableLabels {
			matchRoutes, ok := virtualServiceMatchRoute(svcName, relatedDeployments, label, envHeaderName,
				envHeaderAliases, envSplitter, port, toSubsetName(defaultSubset))
			if ok {
				for _, matchRoute := range matchRoutes {
					mergedRoute, err := mergeRoute(httpRoute, &matchRoute)
					if mergedRoute != nil {
						virtualSvc.Spec.HTTP = append(virtualSvc.Spec.HTTP, *mergedRoute)
					} else {
						logger.Error(err, "Failed to merge route")
					}
				}
			}
		}
		route := defaultRoute(svcName, port, toSubsetName(defaultSubset))
		mergedRoute, err := mergeRoute(httpRoute, &route)
		if mergedRoute != nil {
			virtualSvc.Spec.HTTP = append(virtualSvc.Spec.HTTP, *mergedRoute)
		} else {
			logger.Error(err, "Failed to merge default route")
		}
	}
	return virtualSvc
}

// delete VirtualService
func DeleteVirtualService(client client.Client, namespace string, name string) error {
	return shared.DeleteIns(client, namespace, name, &networkingv1alpha3.VirtualService{})
}

// check whether VirtualService is different
func IsDifferentVirtualService(spec1 *networkingv1alpha3.VirtualServiceSpec,
	spec2 *networkingv1alpha3.VirtualServiceSpec) bool {
	if !reflect.DeepEqual(spec1.Gateways, spec2.Gateways) {
		return true
	}
	if !reflect.DeepEqual(spec1.Hosts, spec2.Hosts) {
		return true
	}
	if len(spec1.HTTP) != len(spec2.HTTP) {
		return true
	}
	for _, route1 := range spec1.HTTP {
		if !findMatchRoute(spec2.HTTP, &route1) {
			return true
		}
	}
	return false
}

// check whether HTTPRoute exist in list
func findMatchRoute(routes []networkingv1alpha3.HTTPRoute, target *networkingv1alpha3.HTTPRoute) bool {
	for _, route := range routes {
		if isRouteEqual(&route, target) {
			return true
		}
	}
	return false
}

// compare whether route rule is equal
func isRouteEqual(route *networkingv1alpha3.HTTPRoute, target *networkingv1alpha3.HTTPRoute) bool {
	if route.Match == nil || target.Match == nil {
		return route.Match == nil && target.Match == nil && isDestinationEqual(route, target)
	} else if len(route.Match) == 0 || len(target.Match) == 0 {
		return len(route.Match) == 0 && len(target.Match) == 0 && isDestinationEqual(route, target)
	} else {
		return isMatchHeadersEqual(route.Match[0].Headers, target.Match[0].Headers) && isDestinationEqual(route, target)
	}
}

// compare whether header matcher is equal
func isMatchHeadersEqual(headers map[string]v1alpha1.StringMatch, targetHeaders map[string]v1alpha1.StringMatch) bool {
	for _, header := range headers {
		found := false
		for _, anotherHeader := range targetHeaders {
			if reflect.DeepEqual(header, anotherHeader) {
				found = true
				break
			}
		}
		if found != true {
			return false
		}
	}
	return true
}

// compare whether route destination is equal
func isDestinationEqual(route *networkingv1alpha3.HTTPRoute, target *networkingv1alpha3.HTTPRoute) bool {
	return route.Route[0].Destination.Subset == target.Route[0].Destination.Subset &&
		route.Route[0].Destination.Port.Number == target.Route[0].Destination.Port.Number
}

// calculate and generate http route instance
func virtualServiceMatchRoute(serviceName string, relatedDeployments []string, labelVal string,
	headerKeyName string, headerKeyAlias []envv1alpha2.EnvHeaderAliasSpec, splitter string, port uint32,
	defaultSubset string) ([]networkingv1alpha3.HTTPRoute, bool) {
	possibleRoutes := getPossibleRoutes(relatedDeployments, labelVal, splitter)
	if len(possibleRoutes) > 0 {
		var subsetName = toSubsetName(findLongestString(possibleRoutes))
		if defaultSubset != subsetName {
			var routes = []networkingv1alpha3.HTTPRoute{
				matchRouteExact(serviceName, headerKeyName, labelVal, port, subsetName),
			}
			if headerKeyAlias != nil {
				for _, alias := range headerKeyAlias {
					var route networkingv1alpha3.HTTPRoute
					if alias.Pattern != "" && alias.Placeholder != "" {
						regexLabelVal := strings.ReplaceAll(alias.Pattern, alias.Placeholder, labelVal)
						route = matchRouteRegex(serviceName, alias.Name, regexLabelVal, port, subsetName)
					} else {
						route = matchRouteExact(serviceName, alias.Name, labelVal, port, subsetName)
					}
					routes = append(routes, route)
				}
			}
			return routes, true
		}
	}
	return []networkingv1alpha3.HTTPRoute{}, false
}

// fetch all route rule for specified label
func getPossibleRoutes(relatedDeployments []string, labelVal string, splitter string) []string {
	var possibleRoutes []string
	for _, v := range relatedDeployments {
		if leveledEqual(labelVal, v, splitter) {
			possibleRoutes = append(possibleRoutes, v)
		}
	}
	return possibleRoutes
}

// generate default http route instance
func defaultRoute(name string, port uint32, defaultSubset string) networkingv1alpha3.HTTPRoute {
	return networkingv1alpha3.HTTPRoute{
		Match: []networkingv1alpha3.HTTPMatchRequest{{
			Port: port,
		}},
		Route: generateHttpRoute(name, port, defaultSubset),
	}
}

// generate istio virtual service http route instance with regex match
func matchRouteRegex(serviceName string, headerKey string, labelVal string, port uint32,
	subsetName string) networkingv1alpha3.HTTPRoute {
	route := matchRoute(serviceName, port, subsetName)
	route.Match[0].Headers[headerKey] = v1alpha1.StringMatch{Regex: labelVal}
	return route
}

// generate istio virtual service http route instance with exact match
func matchRouteExact(serviceName string, headerKey string, labelVal string, port uint32,
	subsetName string) networkingv1alpha3.HTTPRoute {
	route := matchRoute(serviceName, port, subsetName)
	route.Match[0].Headers[headerKey] = v1alpha1.StringMatch{Exact: labelVal}
	return route
}

// generate istio virtual service http route instance
func matchRoute(serviceName string, port uint32, subsetName string) networkingv1alpha3.HTTPRoute {
	route := networkingv1alpha3.HTTPRoute{
		Route: generateHttpRoute(serviceName, port, subsetName),
		Match: []networkingv1alpha3.HTTPMatchRequest{{
			Headers: map[string]v1alpha1.StringMatch{},
			Port:    port,
		}},
	}
	return route
}

// generate istio route
func generateHttpRoute(serviceName string, port uint32, subsetName string) []networkingv1alpha3.HTTPRouteDestination {
	return []networkingv1alpha3.HTTPRouteDestination{{
		Destination: networkingv1alpha3.Destination{
			Host:   serviceName,
			Subset: subsetName,
			Port:   networkingv1alpha3.PortSelector{Number: port},
		},
		Weight: 100,
	}}
}

// get the longest string in list
func findLongestString(strings []string) string {
	mostLongStr := ""
	for _, str := range strings {
		if len(str) > len(mostLongStr) {
			mostLongStr = str
		}
	}
	return mostLongStr
}

// check whether source string match target string at any level
func leveledEqual(source string, target string, splitter string) bool {
	for {
		if source == target {
			return true
		}
		if strings.Contains(source, splitter) {
			source = source[0:strings.LastIndex(source, splitter)]
		} else {
			return false
		}
	}
}
