// 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 proxy

import (
	"fmt"
	"regexp"
	"strings"

	"istio.io/api/networking/v1alpha3"

	apiv1 "github.com/alibaba/higress/v2/api/networking/v1"
	"github.com/alibaba/higress/v2/pkg/common"
	ingress "github.com/alibaba/higress/v2/pkg/ingress/kube/common"
	"github.com/alibaba/higress/v2/pkg/ingress/kube/util"
)

const (
	proxyPortRangeStart uint32 = 50001
	proxyPortRangeEnd   uint32 = 51000 // Exclusive

	defaultProxyConnectTimeout = 1200

	proxyClusterPatchTemplate = `{
  "name": "{{name}}",
  "connect_timeout": "{{connect_timeout}}ms",
  "type": "{{type}}",
  "dns_lookup_family": "V4_ONLY",
  "load_assignment": {
    "cluster_name": "{{name}}",
    "endpoints": [
      {
        "lb_endpoints": [
          {
            "endpoint": {
              "address": {
                "socket_address": {
                  "address": "{{address}}",
                  "port_value": {{port}}
                }
              }
            }
          }
        ]
      }
    ]
  }
}`
	proxyListenerPatchTemplate = `{
  "name": "istio-autogenerated-proxy-listener-{{proxy_name}}",
  "address": {
    "socket_address": {
      "address": "127.0.0.1",
      "port_value": {{port}}
    }
  },
  "listener_filters": [
    {
      "name": "envoy.filters.listener.tls_inspector",
      "typed_config": {
        "@type": "type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector"
      }
    }
  ],
  "filter_chains": [
    {
      "filters": [
        {
          "name": "tcp",
          "typed_config": {
            "@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
            "stat_prefix": "tcpproxy.{{proxy_name}}",
            "cluster": "{{cluster_name}}",
            "tunneling_config": {
              "hostname": "%REQUESTED_SERVER_NAME%:443"
            }
          }
        }
      ]
    }
  ]
}`
)

var (
	configPatchesBuilders = map[common.ProxyType]func(*apiv1.ProxyConfig) []*v1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{
		common.ProxyType_HTTP: buildConfigPatchesForHttpProxy,
	}

	ipv4AddressRegexp = regexp.MustCompile("(\\b25[0-5]|\\b2[0-4][0-9]|\\b[01]?[0-9][0-9]?)(\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}")
)

func NeedToFillProxyListenerPorts(proxies []*apiv1.ProxyConfig) bool {
	if proxies == nil || len(proxies) == 0 {
		return false
	}
	for _, proxy := range proxies {
		if proxy.ListenerPort <= 0 {
			return true
		}
	}
	return false
}

func FillProxyListenerPorts(proxies []*apiv1.ProxyConfig) bool {
	if proxies == nil || len(proxies) == 0 {
		return false
	}
	filled := false
	usedPorts := make(map[uint32]bool)
	for _, proxy := range proxies {
		if proxy.ListenerPort > 0 {
			usedPorts[proxy.ListenerPort] = true
		}
	}
	for _, proxy := range proxies {
		if proxy.ListenerPort > 0 {
			continue
		}
		for port := proxyPortRangeStart; port < proxyPortRangeEnd; port++ {
			if !usedPorts[port] {
				proxy.ListenerPort = port
				usedPorts[port] = true
				filled = true
				break
			}
		}
	}
	return filled
}

func BuildProxyWrapper(config *apiv1.ProxyConfig) *ingress.ProxyWrapper {
	if config == nil {
		return nil
	}
	if len(config.ServerAddress) == 0 || config.ServerPort <= 0 || config.ServerPort > 65535 || config.ListenerPort <= 0 || config.ListenerPort > 65535 {
		return nil
	}

	envoyFilter := buildEnvoyFilter(config)
	if envoyFilter == nil {
		return nil
	}

	return &ingress.ProxyWrapper{
		ProxyName:    config.Name,
		ListenerPort: config.ListenerPort,
		EnvoyFilter:  envoyFilter,
	}
}

func buildEnvoyFilter(config *apiv1.ProxyConfig) *v1alpha3.EnvoyFilter {
	if config == nil {
		return nil
	}

	configPatchesBuilder := configPatchesBuilders[common.ParseProxyType(config.Type)]
	if configPatchesBuilder == nil {
		return nil
	}

	configPatches := configPatchesBuilder(config)
	return &v1alpha3.EnvoyFilter{ConfigPatches: configPatches}
}

func buildConfigPatchesForHttpProxy(config *apiv1.ProxyConfig) []*v1alpha3.EnvoyFilter_EnvoyConfigObjectPatch {
	if common.ParseProxyType(config.Type) != common.ProxyType_HTTP {
		return nil
	}

	clusterName := buildClusterName(config)

	var patches []*v1alpha3.EnvoyFilter_EnvoyConfigObjectPatch

	// Add a cluster for the proxy  server
	proxyClusterPatchJson := proxyClusterPatchTemplate
	{
		clusterType := ""
		if ipv4AddressRegexp.MatchString(config.ServerAddress) {
			clusterType = "STATIC"
		} else {
			clusterType = "STRICT_DNS"
		}
		connectTimeout := config.ConnectTimeout
		if connectTimeout <= 0 {
			connectTimeout = defaultProxyConnectTimeout
		}
		proxyClusterPatchJson = strings.ReplaceAll(proxyClusterPatchJson, "{{name}}", clusterName)
		proxyClusterPatchJson = strings.ReplaceAll(proxyClusterPatchJson, "{{type}}", clusterType)
		proxyClusterPatchJson = strings.ReplaceAll(proxyClusterPatchJson, "{{connect_timeout}}", fmt.Sprintf("%d", connectTimeout))
		proxyClusterPatchJson = strings.ReplaceAll(proxyClusterPatchJson, "{{address}}", config.ServerAddress)
		proxyClusterPatchJson = strings.ReplaceAll(proxyClusterPatchJson, "{{port}}", fmt.Sprintf("%d", config.ServerPort))
	}
	proxyClusterPatch := &v1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{
		ApplyTo: v1alpha3.EnvoyFilter_CLUSTER,
		Match: &v1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{
			Context: v1alpha3.EnvoyFilter_GATEWAY,
		},
		Patch: &v1alpha3.EnvoyFilter_Patch{
			Operation: v1alpha3.EnvoyFilter_Patch_ADD,
			Value:     util.BuildPatchStruct(proxyClusterPatchJson),
		},
	}
	patches = append(patches, proxyClusterPatch)

	// Add a listener to accept requests from the gateway itself
	proxyListenerPatchJson := proxyListenerPatchTemplate
	proxyListenerPatchJson = strings.ReplaceAll(proxyListenerPatchJson, "{{proxy_name}}", config.Name)
	proxyListenerPatchJson = strings.ReplaceAll(proxyListenerPatchJson, "{{port}}", fmt.Sprintf("%d", config.ListenerPort))
	proxyListenerPatchJson = strings.ReplaceAll(proxyListenerPatchJson, "{{cluster_name}}", clusterName)
	proxyListenerPatch := &v1alpha3.EnvoyFilter_EnvoyConfigObjectPatch{
		ApplyTo: v1alpha3.EnvoyFilter_LISTENER,
		Match: &v1alpha3.EnvoyFilter_EnvoyConfigObjectMatch{
			Context: v1alpha3.EnvoyFilter_GATEWAY,
		},
		Patch: &v1alpha3.EnvoyFilter_Patch{
			Operation: v1alpha3.EnvoyFilter_Patch_ADD,
			Value:     util.BuildPatchStruct(proxyListenerPatchJson),
		},
	}
	patches = append(patches, proxyListenerPatch)

	return patches
}

func buildClusterName(config *apiv1.ProxyConfig) string {
	if config == nil {
		return ""
	}
	return fmt.Sprintf("outbound|%d||%s.proxy", config.ServerPort, config.Name)
}
