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

import (
	"bytes"
	"fmt"

	"istio.io/istio/pkg/kube"
	"istio.io/istio/pkg/test/framework/config"
)

// Map can be given as a shared reference to multiple Topology/Cluster implementations.
// allowing clusters to find each other for lookups of Primary, ConfigCluster, etc.
type Map = map[string]Cluster

func NewTopology(config Config, allClusters Map) Topology {
	return Topology{
		ClusterName:             config.Name,
		ClusterKind:             config.Kind,
		Network:                 config.Network,
		ClusterHTTPProxy:        config.HTTPProxy,
		PrimaryClusterName:      config.PrimaryClusterName,
		ConfigClusterName:       config.ConfigClusterName,
		ClusterProxyKubectlOnly: config.ProxyKubectlOnly,
		AllClusters:             allClusters,
		Index:                   len(allClusters),
		ConfigMetadata:          config.Meta,
	}
}

// Topology gives information about the relationship between clusters.
// Cluster implementations can embed this struct to include common functionality.
type Topology struct {
	ClusterName             string
	ClusterKind             Kind
	Network                 string
	ClusterHTTPProxy        string
	PrimaryClusterName      string
	ConfigClusterName       string
	ClusterProxyKubectlOnly bool
	Index                   int
	// AllClusters should contain all AllClusters in the context
	AllClusters    Map
	ConfigMetadata config.Map
}

// MetadataValue provides the configured value for a metadata key in the cluster configuration.
func (c Topology) MetadataValue(key string) string {
	return c.ConfigMetadata.String(key)
}

// NetworkName the cluster is on
func (c Topology) NetworkName() string {
	return c.Network
}

// Name provides the ClusterName this cluster used by Istio.
func (c Topology) Name() string {
	return c.ClusterName
}

// HTTPProxy to connect to the cluster
func (c Topology) HTTPProxy() string {
	return c.ClusterHTTPProxy
}

func (c Topology) ProxyKubectlOnly() bool {
	return c.ClusterProxyKubectlOnly
}

// knownClusterNames maintains a well-known set of cluster names. These will always be used with
// StableNames if encountered.
var knownClusterNames = map[string]struct{}{
	"primary":               {},
	"remote":                {},
	"cross-network-primary": {},
}

// StableName provides a name used for testcase names. Deterministic, so testgrid
// can be consistent when the underlying cluster names are dynamic.
func (c Topology) StableName() string {
	var prefix string
	switch c.Kind() {
	case Kubernetes:
		// If its a known cluster name, use that directly.
		// This will not be dynamic, and allows 1:1 correlation of cluster name and test name for simplicity.
		if _, f := knownClusterNames[c.Name()]; f {
			return c.Name()
		}
		if c.IsPrimary() {
			if c.IsConfig() {
				prefix = "primary"
			} else {
				prefix = "externalistiod"
			}
		} else if c.IsRemote() {
			if c.IsConfig() {
				prefix = "config"
			} else {
				prefix = "remote"
			}
		}
	default:
		prefix = string(c.Kind())
	}

	return fmt.Sprintf("%s-%d", prefix, c.Index)
}

func (c Topology) Kind() Kind {
	return c.ClusterKind
}

func (c Topology) IsPrimary() bool {
	return c.Primary().Name() == c.Name()
}

func (c Topology) IsConfig() bool {
	return c.Config().Name() == c.Name()
}

func (c Topology) IsRemote() bool {
	return !c.IsPrimary()
}

func (c Topology) IsExternalControlPlane() bool {
	return c.IsPrimary() && !c.IsConfig()
}

func (c Topology) Primary() Cluster {
	cluster, ok := c.AllClusters[c.PrimaryClusterName]
	if !ok || cluster == nil {
		panic(fmt.Errorf("cannot find %s, the primary cluster for %s", c.PrimaryClusterName, c.Name()))
	}
	return cluster
}

func (c Topology) PrimaryName() string {
	return c.PrimaryClusterName
}

func (c Topology) Config() Cluster {
	cluster, ok := c.AllClusters[c.ConfigClusterName]
	if !ok || cluster == nil {
		panic(fmt.Errorf("cannot find %s, the config cluster for %s", c.ConfigClusterName, c.Name()))
	}
	return cluster
}

func (c Topology) ConfigName() string {
	return c.ConfigClusterName
}

func (c Topology) WithPrimary(primaryClusterName string) Topology {
	// TODO remove this, should only be provided by external config
	c.PrimaryClusterName = primaryClusterName
	return c
}

func (c Topology) WithConfig(configClusterName string) Topology {
	// TODO remove this, should only be provided by external config
	c.ConfigClusterName = configClusterName
	return c
}

func (c Topology) MinKubeVersion(minor uint) bool {
	cluster := c.AllClusters[c.ClusterName]
	if cluster.Kind() != Kubernetes && cluster.Kind() != Fake {
		return c.Primary().MinKubeVersion(minor)
	}
	return kube.IsAtLeastVersion(cluster, minor)
}

func (c Topology) MaxKubeVersion(minor uint) bool {
	cluster := c.AllClusters[c.ClusterName]
	if cluster.Kind() != Kubernetes && cluster.Kind() != Fake {
		return c.Primary().MaxKubeVersion(minor)
	}
	return kube.IsLessThanVersion(cluster, minor+1)
}

func (c Topology) String() string {
	buf := &bytes.Buffer{}

	_, _ = fmt.Fprintf(buf, "Name:               %s\n", c.Name())
	_, _ = fmt.Fprintf(buf, "StableName:         %s\n", c.StableName())
	_, _ = fmt.Fprintf(buf, "Kind:               %s\n", c.Kind())
	_, _ = fmt.Fprintf(buf, "PrimaryCluster:     %s\n", c.Primary().Name())
	_, _ = fmt.Fprintf(buf, "ConfigCluster:      %s\n", c.Config().Name())
	_, _ = fmt.Fprintf(buf, "Network:            %s\n", c.NetworkName())
	_, _ = fmt.Fprintf(buf, "HTTPProxy:          %s\n", c.HTTPProxy())
	_, _ = fmt.Fprintf(buf, "ProxyKubectlOnly:   %t\n", c.ProxyKubectlOnly())

	return buf.String()
}
