package controllers

import (
	"fmt"
	"log"
	"regexp"
	"strings"

	v1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1"
	gardencoreclientset "github.com/gardener/gardener/pkg/client/core/clientset/versioned"
	"github.com/hashicorp/go-multierror"
	"go.uber.org/zap"
	"helm.sh/helm/v3/pkg/action"
	"helm.sh/helm/v3/pkg/cli"
	v1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

var (
	_                ShootControllerInterface = new(ShootLoggingController)
	pemCommentsRegex                          = regexp.MustCompile("-----.+?-----")
)

const (
	ClientAuthSecret          = "client-auth-cert"
	FluentbitReleaseNamespace = "logging"
	LokiReleaseName           = "loki"
	DashboardReleaseName      = "dashboard"
	DashboardNamespace        = "kubernetes-dashboard"
)

type ShootLoggingController struct {
	GardenCli           gardencoreclientset.Interface
	Clientset           kubernetes.Interface
	GardenerClient      kubernetes.Interface
	authSecret          *v1.Secret
	Log                 *zap.Logger
	LokiIngressHostName string
	LokiStackChartPath  string
	GardenerProjectName *string
	// Shoot cluster clientset will created by `/tmp/shoot_id` kubeconfig file.
	// because we will create some resources in shoot namespace, so we must injest a fake clientset to testing
	fakeShootClientSet kubernetes.Interface
	dryRun             bool

	DashboardChartPath string
	PortalAuth0Issuer  string
	PortalClientID     string

	AwsESPwd             string
	AwsESHost            string
	EventrouterChartPath string

	SlowQueryTiDBHost     string
	SlowQueryTiDBUser     string
	SlowQueryTiDBPassword string
	SlowQueryTiDBDatabase string
	SlowQueryTiDBCA       string
	SlowQueryTiDBCert     string
	SlowQueryTiDBKey      string
}

func (slc *ShootLoggingController) Update(namespace string, name string, tenantID int64, cloud string, region string) error {
	logger := slc.Log.With(zap.String("name", name)).With(zap.String("region", region))
	shoot, _ := slc.GardenCli.CoreV1beta1().Shoots(namespace).Get(name, metav1.GetOptions{})
	if enableHibernation(shoot) {
		logger.Warn("shoot hibernated", zap.String("shoot_obj_name", shoot.Name))
		return nil
	}
	configPath := fmt.Sprintf("/tmp/%s", name)
	if err := slc.writeShootKubeConfigToTmpDir(logger, namespace, name, configPath); err != nil {
		logger.Error("write shoot kubeconfig to tmp", zap.Error(err))
		return err
	}
	var errs *multierror.Error
	if err := slc.deployFluentBit(logger, configPath, cloud, region, name); err != nil {
		logger.Error("deploy fluent-bit", zap.Error(err))
		errs = multierror.Append(errs, err)
	}

	if err := slc.deployDashboard(logger, configPath); err != nil {
		logger.Error("deploy kubernetes dashboard", zap.Error(err))
		errs = multierror.Append(errs, err)
	}

	if err := slc.deployEventrouter(logger, configPath); err != nil {
		logger.Error("deploy eventrouter", zap.Error(err))
		errs = multierror.Append(errs, err)
	}
	return errs.ErrorOrNil()
}

func enableHibernation(shoot *v1beta1.Shoot) bool {
	if shoot != nil && shoot.Spec.Hibernation != nil && shoot.Spec.Hibernation.Enabled != nil && *shoot.Spec.Hibernation.Enabled == true {
		return true
	}
	return false
}

func (slc *ShootLoggingController) writeShootKubeConfigToTmpDir(logger *zap.Logger, namespace, name, configPath string) error {
	clientConfig, err := RetrieveShootKubeConfig(slc.GardenerClient, namespace, name)
	if err != nil {
		logger.Error("failed retrieve shoot kube config", zap.Error(err))
		return err
	}

	rawConfig, err := clientConfig.RawConfig()
	if err != nil {
		logger.Error("get row config", zap.Error(err))
		return err
	}

	err = clientcmd.WriteToFile(rawConfig, configPath)
	if err != nil {
		logger.Error("write config failed", zap.Error(err))
		return err
	}
	return nil
}

func (slc *ShootLoggingController) Delete(namespace string, name string) error {
	// fluent-bit is stateless, so we don't have to do anything when shoot deleted
	slc.Log.Info("shoot deleted", zap.String("namespace", namespace), zap.String("name", name))
	return nil
}

func (slc *ShootLoggingController) clientAuthSecret() (*v1.Secret, error) {
	return slc.Clientset.CoreV1().Secrets("kube-system").Get(ClientAuthSecret, metav1.GetOptions{})
}

func initHelmActionConfig(kubeConfigPath string, namespace string) (*action.Configuration, error) {
	settings := cli.New()
	settings.KubeConfig = kubeConfigPath
	actionConfig := new(action.Configuration)
	// You can pass an empty string instead of settings.Namespace() to list
	// all namespaces
	if err := actionConfig.Init(settings.RESTClientGetter(), namespace,
		"secret", log.Printf); err != nil {
		return nil, err
	}
	return actionConfig, nil
}

func (slc *ShootLoggingController) shootClientSet(actionConfig *action.Configuration) (kubernetes.Interface, error) {
	var (
		shootClientSet kubernetes.Interface
		err            error
	)

	if slc.fakeShootClientSet != nil {
		shootClientSet = slc.fakeShootClientSet
	} else {
		shootClientSet, err = actionConfig.KubernetesClientSet()
		if err != nil {
			return nil, err
		}
	}
	return shootClientSet, nil
}
func (slc *ShootLoggingController) deployEventrouter(logger *zap.Logger, kubeConfigPath string) error {
	actionConfig, err := initHelmActionConfig(kubeConfigPath, "kube-system")

	rel, err := HelmUpgrade(actionConfig, "", DashboardReleaseName, slc.EventrouterChartPath, nil, slc.dryRun)
	if err != nil {
		logger.Error("upgrade eventrouter", zap.Error(err))
		return err
	}

	logger.Info("Upgrade eventrouter successful")
	logger.Debug("eventrouter release output", zap.Any("release", rel))
	return nil
}

func (slc *ShootLoggingController) deployDashboard(logger *zap.Logger, kubeConfigPath string) error {
	actionConfig, err := initHelmActionConfig(kubeConfigPath, DashboardNamespace)

	values := map[string]interface{}{
		"oidc": map[string]interface{}{
			"issuer":          slc.PortalAuth0Issuer,
			"allowedClientID": slc.PortalClientID,
		},
	}

	shootClientSet, err := slc.shootClientSet(actionConfig)
	if err != nil {
		logger.Error("new shoot clientset", zap.Error(err))
		return err
	}

	// create namespace if not exist
	_, err = shootClientSet.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: DashboardNamespace}})
	if err != nil && !apierrors.IsAlreadyExists(err) {
		logger.Error("create namespace", zap.Error(err))
		return err
	}

	rel, err := HelmUpgrade(actionConfig, "", DashboardReleaseName, slc.DashboardChartPath, values, slc.dryRun)
	if err != nil {
		logger.Error("upgrade kubernetes dashboard", zap.Error(err))
		return err
	}

	logger.Info("Upgrade dashboard successful")
	logger.Debug("dashboard release output", zap.Any("release", rel))
	return nil
}

func (slc *ShootLoggingController) deployFluentBit(logger *zap.Logger, kubeConfigPath, cloud, region, shootName string) error {
	actionConfig, err := initHelmActionConfig(kubeConfigPath, FluentbitReleaseNamespace)
	if err != nil {
		logger.Error("init action config", zap.Error(err))
		return err
	}

	shootClientSet, err := slc.shootClientSet(actionConfig)
	if err != nil {
		logger.Error("new shoot clientset", zap.Error(err))
		return err
	}

	// create namespace if not exist
	_, err = shootClientSet.CoreV1().Namespaces().Create(&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: FluentbitReleaseNamespace}})
	if err != nil && !apierrors.IsAlreadyExists(err) {
		logger.Error("create namespace", zap.Error(err))
		return err
	}

	// define values, install fluent-bit into shoot cluster
	shootLabel := fmt.Sprintf("shoot/%s/%s/shoot--%s--%s", cloud, region, *slc.GardenerProjectName, shootName)
	vals := map[string]interface{}{
		"loki": map[string]interface{}{
			"enabled": false,
		},
		"fluent-bit": map[string]interface{}{
			"config": map[string]interface{}{
				// The label of the logging system should be consistent with the monitoring system
				"labels": fmt.Sprintf(`{job="fluent-bit",cluster="%s"}`, shootLabel),
			},
			"enableTLS":            true,
			"clientAuthSecretName": "client-auth-cert",
			"loki": map[string]interface{}{
				"serviceName":   slc.LokiIngressHostName,
				"servicePort":   443,
				"serviceScheme": "https",
				"servicePath":   `/loki/api/v1/push`,
			},
		},
		"filebeat": map[string]interface{}{
			"enabled": false,
		},
	}

	rel, err := HelmUpgrade(actionConfig, FluentbitReleaseNamespace, LokiReleaseName, slc.LokiStackChartPath, vals, slc.dryRun)
	if err != nil {
		logger.Error("upgrade fluent-bit", zap.Error(err))
		return err
	}

	logger.Info("upgrade fluent-bit successful")
	logger.Debug("fluent-bit release output", zap.Any("release", rel))
	return nil
}

// PEM format
func ensureCertFormatCorrect(in string) string {
	buf := strings.Builder{}
	matches := pemCommentsRegex.FindAllStringIndex(in, -1)
	// "-----.+?----"- must exist and be paired
	if len(matches) < 1 || len(matches)%2 != 0 {
		return in
	}
	for i := 0; i < len(matches); i += 2 {
		buf.WriteString(in[matches[i][0]:matches[i][1]])
		buf.WriteString("\n")
		body := in[matches[i][1]:matches[i+1][0]]
		split := strings.Split(body, " ")
		buf.WriteString(strings.Join(split, "\n"))
		buf.WriteString("\n")
		buf.WriteString(in[matches[i+1][0]:matches[i+1][1]])
		buf.WriteString("\n")
	}
	return buf.String()
}
