package frpc

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/BurntSushi/toml"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
	frpcConfigMapName = "frpc-configmap"
	frpsPort          = 8080
	eksAPIServerPort  = 443
	frpcAdminPort     = 7400
	frpcAdminEndpoint = "127.0.0.1"
	clusterProxyPort  = 80

	// FRP configurations: https://github.com/fatedier/frp#configuration-files
	frpcConfigFileName = "frpc.ini"

	eksFrpcName         = "eks"
	proxyFrpcName       = "proxy"
	clusterProxySvcName = "proxy-cluster-proxy.tidb-admin"
)

func (f *frpc) FrpcConfigMap(ctx context.Context, cli client.Client, eksEndpoint, frpsAddr string) error {
	if err := f.updateFrpcCM(ctx, cli, clusterProxySvcName, frpsAddr, proxyFrpcName, clusterProxyPort); err != nil {
		return err
	}
	if err := f.updateFrpcCM(ctx, cli, eksEndpoint, frpsAddr, eksFrpcName, eksAPIServerPort); err != nil {
		return err
	}
	return nil
}

func (f *frpc) updateFrpcCM(ctx context.Context, cli client.Client, endpoint, frpsAddr, name string, port int) error {
	cm := f.getFrpcCM(name, endpoint, frpsAddr, port)
	res, err := controllerutil.CreateOrUpdate(ctx, cli, cm, func() error {
		cm.Data = f.getFrpcCM(name, endpoint, frpsAddr, port).Data
		return nil
	})
	if err != nil {
		return err
	}
	if res == controllerutil.OperationResultUpdated {
		pods := &corev1.PodList{}
		err := cli.List(ctx, pods, client.MatchingLabels{frpcLabelKey: fmt.Sprintf("%s-%s", frpcLabelValue, name)})
		if err != nil {
			return err
		}
		for _, pod := range pods.Items {
			err := cli.Delete(ctx, &pod)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func (f *frpc) getFrpcCM(name, localEndpoint, frpsAddr string, localPort int) *corev1.ConfigMap {
	res := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("%s-%s", frpcConfigMapName, name),
			Namespace: metav1.NamespaceSystem,
		},
		Data: map[string]string{frpcConfigFileName: frpcConfig(name, localEndpoint, frpsAddr, localPort, frpsPort)},
	}
	return res
}

// frpcConfig returns the configmap content for frpc.
func frpcConfig(name, localEndpoint, frpsAddr string, localPort, frpsPort int) string {
	type commonConfig struct {
		ServerAddr string `toml:"server_addr"`
		ServerPort int    `toml:"server_port"`
		AdminAddr  string `toml:"admin_addr"`
		AdminPort  int    `toml:"admin_port"`
	}
	type tcpConfig struct {
		Type       string `toml:"type"`
		LocalIP    string `toml:"local_ip"`
		LocalPort  int    `toml:"local_port"`
		RemotePort int    `toml:"remote_port"`
	}
	type frpcConfig struct {
		Common commonConfig `toml:"common"`
		EKS    *tcpConfig   `toml:"eks,omitempty"`
		Proxy  *tcpConfig   `toml:"proxy,omitempty"`
	}
	cfg := &frpcConfig{
		Common: commonConfig{
			ServerAddr: frpsAddr,
			ServerPort: frpsPort,
			AdminAddr:  frpcAdminEndpoint,
			AdminPort:  frpcAdminPort,
		},
	}
	tcpCfg := &tcpConfig{
		Type:       "tcp",
		LocalIP:    localEndpoint,
		LocalPort:  localPort,
		RemotePort: localPort,
	}
	switch name {
	case eksFrpcName:
		cfg.EKS = tcpCfg
	case proxyFrpcName:
		cfg.Proxy = tcpCfg
	}
	buf := new(bytes.Buffer)
	if err := toml.NewEncoder(buf).Encode(cfg); err != nil {
		return ""
	}
	return buf.String()
}

func (f *frpc) ConfigMapData(ctx context.Context, cli client.Client) (string, error) {
	data := make(map[string]map[string]string)
	cm := &corev1.ConfigMap{}
	err := cli.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-%s", frpcConfigMapName, eksFrpcName), Namespace: metav1.NamespaceSystem}, cm)
	if err != nil {
		return "", err
	}
	data[fmt.Sprintf("%s-%s", frpcConfigMapName, eksFrpcName)] = cm.Data
	cm = &corev1.ConfigMap{}
	err = cli.Get(ctx, client.ObjectKey{Name: fmt.Sprintf("%s-%s", frpcConfigMapName, proxyFrpcName), Namespace: metav1.NamespaceSystem}, cm)
	if err != nil {
		return "", err
	}
	data[fmt.Sprintf("%s-%s", frpcConfigMapName, proxyFrpcName)] = cm.Data
	buf, err := json.Marshal(cm.Data)
	if err != nil {
		return "", err
	}
	return string(buf), nil
}
