package configupdate

import (
	"context"
	"fmt"
	"io"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	. "github.com/onsi/gomega"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"golang.org/x/sync/errgroup"

	. "github.com/pingcap/endless/pkg/matcher"
	. "github.com/pingcap/endless/pkg/matcher/metrics"
	. "github.com/pingcap/endless/pkg/util"
	"github.com/pingcap/endless/testcase/oltp/oltppkg"
	"github.com/pingcap/test-infra/sdk/resource"
	_ "github.com/pingcap/test-infra/sdk/resource/impl/k8s"
)

// make sure this path is consistent with the deployment file if deployment file opened encryption
const keyPath = "/var/lib/tikv/master_key"

// will use this key if deploy without encryption
const masterKey = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"

func OLTP_TiKV_DATA_ENCRYPTION_METHOD_UPDATE_TO_SM4(
	ctx context.Context,
	tc resource.TiDBCluster,
	pr *io.PipeReader,
	caseCmdParms oltppkg.CaseCmdParms,
) {
	curVerShort := strings.TrimSuffix(strings.TrimPrefix(oltppkg.GetCompVersion(ctx, tc, "tikv"), "v"), "-pre")
	if oltppkg.CompareVersion(curVerShort, "6.2.0") < 0 {
		log.Info("Versions less than 6.2.0 do not support sm4", zap.String("tikvVersion", curVerShort))
		return
	}
	encryptionMethod := "sm4-ctr"
	keyType := "file"
	var config string
	if !isEncryptionOpen(tc) {
		generateMasterKey(tc, masterKey, keyPath)
		config = fmt.Sprintf("[security.encryption]\ndata-encryption-method = \"%s\"\n[security.encryption.master-key]\ntype = \"%s\"\npath = \"%s\"", encryptionMethod, keyType, keyPath)
	} else {
		config = fmt.Sprintf("[security.encryption]\ndata-encryption-method = \"%s\"", encryptionMethod)
	}

	updateEncryptionConfigTest(ctx, tc, pr, caseCmdParms, config)
}

func OLTP_TiKV_DATA_ENCRYPTION_METHOD_UPDATE_TO_AES(
	ctx context.Context,
	tc resource.TiDBCluster,
	pr *io.PipeReader,
	caseCmdParms oltppkg.CaseCmdParms,
) {
	curVerShort := strings.TrimSuffix(strings.TrimPrefix(oltppkg.GetCompVersion(ctx, tc, "tikv"), "v"), "-pre")
	if oltppkg.CompareVersion(curVerShort, "6.2.0") < 0 {
		//https://github.com/tikv/tikv/pull/12272
		log.Info("Versions less than 6.2.0 do not support enable encryption after cluster running", zap.String("tikvVersion", curVerShort))
		return
	}
	encryptionMethod := "aes256-ctr"
	keyType := "file"
	var config string
	if !isEncryptionOpen(tc) {
		generateMasterKey(tc, masterKey, keyPath)
		config = fmt.Sprintf("[security.encryption]\ndata-encryption-method = \"%s\"\n[security.encryption.master-key]\ntype = \"%s\"\npath = \"%s\"", encryptionMethod, keyType, keyPath)
	} else {
		config = fmt.Sprintf("[security.encryption]\ndata-encryption-method = \"%s\"", encryptionMethod)
	}
	updateEncryptionConfigTest(ctx, tc, pr, caseCmdParms, config)
}

func OLTP_TiKV_DATA_ENCRYPTION_MASTER_KEY_UPDATE(
	ctx context.Context,
	tc resource.TiDBCluster,
	pr *io.PipeReader,
	caseCmdParms oltppkg.CaseCmdParms,
) {
	Expect(isEncryptionOpen(tc)).To(BeTrue())
	keyType := "file"
	currentKeyPath := oltppkg.GetConfigByName(tc, "tikv", "security.encryption.master-key.path")[0].Value
	newMasterKey := "505deb1015ca71be2b73aef0857d55811f352c073b6108d72d9810a30914dff4"
	newKeyPath := "/var/lib/tikv/master_key_new"
	config := fmt.Sprintf("[security.encryption.master-key]\ntype = \"%s\"\npath = \"%s\"\n[security.encryption.previous-master-key]\ntype = \"%s\"\npath = \"%s\"", keyType, newKeyPath, keyType, currentKeyPath)
	generateMasterKey(tc, newMasterKey, newKeyPath)
	updateEncryptionConfigTest(ctx, tc, pr, caseCmdParms, config)
}

func OLTP_TiKV_DATA_ENCRYPTION_CLOSE(
	ctx context.Context,
	tc resource.TiDBCluster,
	pr *io.PipeReader,
	caseCmdParms oltppkg.CaseCmdParms,
) {
	curVerShort := strings.TrimSuffix(strings.TrimPrefix(oltppkg.GetCompVersion(ctx, tc, "tikv"), "v"), "-pre")
	if oltppkg.CompareVersion(curVerShort, "6.2.0") < 0 {
		log.Info("Versions less than 6.2.0 do not support turning off encryption", zap.String("tikvVersion", curVerShort))
		return
	}
	Expect(isEncryptionOpen(tc)).To(BeTrue())
	encryptionMethod := "plaintext"
	config := fmt.Sprintf("[security.encryption]\ndata-encryption-method = \"%s\"", encryptionMethod)
	updateEncryptionConfigTest(ctx, tc, pr, caseCmdParms, config)
}

func OLTP_TiKV_DATA_ENCRYPTION_METHOD_UPDATE_REPEATEDLY(
	ctx context.Context,
	tc resource.TiDBCluster,
	pr *io.PipeReader,
	caseCmdParms oltppkg.CaseCmdParms,
) {
	var encryptionMmethods = []string{"aes128-ctr", "aes256-ctr", "plaintext", "sm4", "aes192-ctr", "sm4", "plaintext"}
	rotationPeriod := "1h"
	keyType := "file"
	for _, encryptionMmethod := range encryptionMmethods {
		config := fmt.Sprintf("[security.encryption]\ndata-encryption-method = \"%s\"\ndata-key-rotation-period = \"%s\"\n[security.encryption.master-key]\ntype = \"%s\"\npath = \"%s\"", encryptionMmethod, rotationPeriod, keyType, keyPath)
		generateMasterKey(tc, masterKey, keyPath)
		updateEncryptionConfigTest(ctx, tc, pr, caseCmdParms, config)
	}
}

func updateEncryptionConfigTest(
	ctx context.Context,
	tc resource.TiDBCluster,
	pr *io.PipeReader,
	caseCmdParms oltppkg.CaseCmdParms,
	config string,
) {
	oltppkg.LogAndBy("update encryption config")
	warmUpTime := 30 * time.Minute
	updateConfigTimeout := 60 * time.Minute
	observeTime := 30 * time.Minute
	maxRunTime := warmUpTime + updateConfigTimeout + observeTime

	workloadCheckParms := oltppkg.WorkloadCheckParameters{
		IsCheck:   true,
		IgnoreErr: append(strings.Split(caseCmdParms.TpccParms.IgnoreErrors, ","), "invalid connection", "err dial tcp"),
	}
	qpsJitterCheckParms := oltppkg.QpsJitterCheckParameters{
		IsCheck:      true,
		Offset:       "2m",
		ToleratePerc: 0.5,
		TolerateDur:  30 * time.Second,
	}
	durJitterCheckParms := oltppkg.DurJitterCheckParameters{
		IsCheck: false,
	}

	restartCheckParms := oltppkg.RestartCheckParameters{
		AllComponentRestartCheck: false,
		TiKVRestartCheck:         false,
		PDRestartCheck:           true,
		TiDBRestartCheck:         true,
	}

	ctxAll, cancel := context.WithTimeout(ctx, maxRunTime)
	defer cancel()
	g, gctx := errgroup.WithContext(ctxAll)

	oltppkg.FunCommonCheck(gctx, tc, g, maxRunTime, pr, qpsJitterCheckParms, durJitterCheckParms, restartCheckParms, workloadCheckParms)

	g.Go(WithGinkgoRecover(func() {
		log.Info("tikv restart check during warm up")
		ConsistentlyWithMetrics(tc, warmUpTime).ShouldNot(TiKVRestart().WithCtx(gctx))
		log.Info("tikv restart check done during warm up")
	}))

	oltppkg.WaitFor(gctx, "Warm Up before updating tc config", warmUpTime)

	g.Go(WithGinkgoRecover(func() {
		oltppkg.RollingUpdateConfig(tc, resource.TiKV, config, updateConfigTimeout)
		oltppkg.WaitFor(ctx, "wait before observation check", 3*time.Minute)
		log.Info("tikv restart check during observation after updating tc config", zap.Duration("observeTime", observeTime))
		ConsistentlyWithMetrics(tc, observeTime).ShouldNot(TiKVRestart().WithCtx(gctx))
		log.Info("tikv restart check done during observation")
		cancel()
	}))

	Expect(g.Wait()).Should(BeNil())
}

func generateMasterKey(tc resource.TiDBCluster, masterKey string, keyPath string) {
	generateMasterKey := fmt.Sprintf("echo %s > %s", masterKey, keyPath)
	instanceIdList := oltppkg.GetInstanceIdList(tc, resource.TiKV)
	for _, instanceId := range instanceIdList {
		queryKeyResult, _ := oltppkg.ExecTiKVCmd(tc, instanceId, fmt.Sprintf("ls %s | wc -l", keyPath))
		if queryKeyResult == "0" {
			log.Info("master key is not exist, generate master key")
			Try(oltppkg.ExecTiKVCmd(tc, instanceId, generateMasterKey))
		}
	}
}

func isEncryptionOpen(tc resource.TiDBCluster) bool {
	configRows := oltppkg.GetConfigByName(tc, "tikv", "security.encryption.data-encryption-method")
	isEncryptionOpen := true
	for _, configRow := range configRows {
		currentValue := configRow.Value
		if currentValue == "plaintext" {
			isEncryptionOpen = false
		}
	}
	return isEncryptionOpen
}
