package main

import (
	"context"
	"flag"

	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/dbinit"
	api "github.com/pingcap/dbaas/central/infra-api"
	"github.com/pingcap/dbaas/central/model/cluster/clusterproxy/tidbcloud"
	modelutil "github.com/pingcap/dbaas/central/model/util"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/secret/encrypt"
	secretprovider "github.com/tidbcloud/pkgv2/secret/provider"

	"github.com/pingcap/errors"
	uuid "github.com/satori/go.uuid"
	"go.uber.org/multierr"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
)

const (
	defaultConfigPath = "/etc/dbaas/config.yaml"
)

var (
	dryrun     bool
	clusterID  uint64
	configPath string
)

func init() {
	flag.StringVar(&configPath, "config-path", defaultConfigPath, "dbaas configuration file path")
	flag.Uint64Var(&clusterID, "cluster-id", 0, "cluster id that needs to migrate")
	flag.BoolVar(&dryrun, "dryrun", true, "dryrun to check cluster needs to migrate")
	flag.Parse()
}

func main() {
	if err := startup(); err != nil {
		panic(err)
	}
}

func startup() error {
	ctx := context.Background()
	logger := log.WithContextS(ctx)
	logger.Infof("initialize configuration %s", configPath)
	if err := config.InitConfig(configPath); err != nil {
		logger.Fatal("Initialize configuration failed", zap.Error(err))
	}

	logger.Info("initialize MySQL connection")
	if _, err := dbinit.Initialize(config.Global().MysqlConfig, false); err != nil {
		logger.Fatal("failed to initialize the database", zap.Error(err))
	}

	// gardener cluster k8s client
	var cfg *rest.Config
	var err error
	kubeConfig := config.Global().GardenerConfig.KubeConfigPath
	if kubeConfig == "" {
		cfg, err = rest.InClusterConfig()
	} else {
		cfg, err = clientcmd.BuildConfigFromFlags("", kubeConfig)
	}
	if err != nil {
		return errors.Annotate(err, "initialize the kubernetes config failed")
	}

	// Initialize the k8s/gardener clients.
	baseKubeCli, err := kubernetes.NewForConfig(cfg)
	if err != nil {
		return errors.Annotate(err, "initialize the base kubeclient failed")
	}
	infraAPI := api.InitInfraAPI(baseKubeCli)

	CMKArn := config.Global().InfraConfig.CMKArn
	enc, err := secretprovider.NewSecretEncryptor(CMKArn)
	if err != nil {
		return errors.AddStack(err)
	}
	tx := db.NewProvider().ReadDB(ctx)
	clusters := []entity.Cluster{}
	activeClusters := entity.NewClusterQuerySet(tx).DeletedTimeIsNull().CreationFinishedTimeIsNotNull().StatusEq(entity.ClusterStatusNormal)
	if err := activeClusters.All(&clusters); err != nil {
		return errors.AddStack(err)
	}
	var errs error
	for _, cluster := range clusters {
		if clusterID == 0 || uint64(cluster.ID) == clusterID {
			logger.Infof("Start migrating cluster %d", cluster.ID)
			errs = multierr.Append(errs, errors.AddStack(
				initCluster(ctx, logger, tx, infraAPI, enc, cluster),
			))
			logger.Desugar().Info("finish cluster", zap.Uint("clusterId", uint(cluster.ID)))
			continue
		}
		logger.Infof("Skipping cluster %d", cluster.ID)

	}
	return errs
}

// Manually check if extra-tidb exist to run SEM setup
// As the following SEM setup relies on extra tidb
// Check this metrics sum(kube_pod_labels{pod=~".*tidb-extra-access.*"}) by (namespace,pod,cluster)
func initCluster(ctx context.Context, logger *zap.SugaredLogger, db *gorm.DB, infraapi *api.API, encryptor encrypt.Encryptor, cluster entity.Cluster) error {
	cr, err := infraapi.GetCluster(ctx, cluster.ProjectID.Uint64(), cluster.ID.Uint64())
	if err != nil {
		return errors.Annotate(err, "get cluster from infra-api")
	}

	if cluster.EncryptedCloudAdminPassword == "" {
		logger.Infof("dryrun %t: initCluster add admin password %d", dryrun, cluster.ID.Uint64())
		if !dryrun { // generate cloudadmin password if not dryrun
			adminPwd := encrypt.Secret(uuid.NewV4().String())

			encryptedCloudAdminPassword, err := encrypt.EncryptBase64(encryptor, adminPwd)
			if err != nil {
				return errors.Annotate(err, "encrypt cloud admin password")
			}
			cluster.EncryptedCloudAdminPassword = encryptedCloudAdminPassword
			return db.Transaction(func(tx *gorm.DB) error {
				return cluster.Update(tx, entity.ClusterDBSchema.EncryptedCloudAdminPassword)
			})
		}
	}

	isSEM, err := modelutil.SemVersionCheck(cr.Status)
	if err != nil {
		return err
	}
	if isSEM {
		logger.Infof("dryrun %t: setup sem for cluster %d", dryrun, cluster.ID)
		if !dryrun { // set SEM (cloudadmin password and permission setup) for cluster if not dryrun
			adminPwd, err := modelutil.DecryptCloudAdminPassword(encryptor, cluster, cr.Status)
			if err != nil {
				return errors.AddStack(err)
			}
			if adminPwd == "" {
				return errors.Errorf("empty admin password")
			}

			timeout := 5                    // 5 seconds
			cr.Status.DB.Version = "v5.0.6" // force proxy uses extra-tidb
			proxy, err := tidbcloud.BuildClusterProxy(cluster.ID.Uint64(), cr.Status, timeout)
			if err != nil {
				return err
			}

			logger.Infof("Create cloud_admin user for cluster %d", cluster.ID)
			if err := proxy.ExecuteSQL(ctx, encrypt.Secret(""), "CREATE USER IF NOT EXISTS cloud_admin"); err != nil {
				logger.Errorf("failed to create cloud_admin user %+v", err)
				return err
			}
			logger.Infof("Create cloud_admin password for cluster %d", cluster.ID)
			if err := proxy.CreateCloudAdmin(ctx, adminPwd); err != nil {
				logger.Warnf("failed to set cloud_admin user password %+v", err)
			}
			logger.Infof("Setup SEM permissions for cluster %d", cluster.ID)
			for _, line := range semSetupSQL {
				if err := proxy.ExecuteSQL(ctx, encrypt.Secret(""), line); err != nil {
					logger.Warnf("failed to execute SEM permissions setup %s %+v", line, err)
				}
			}
			logger.Infof("Enable redact log for cluster %d", cluster.ID)
			if err := proxy.ExecuteSQL(ctx, adminPwd, enableRedactLog); err != nil {
				logger.Warnf("failed to enable redact log %+v", err)
			}
		}
	}

	return nil
}

var (
	semSetupSQL = []string{
		"GRANT BACKUP_ADMIN, RESTORE_ADMIN ON *.* TO 'cloud_admin'@'%';",
		"GRANT DASHBOARD_CLIENT on *.* TO 'cloud_admin'@'%';",

		"GRANT SYSTEM_VARIABLES_ADMIN ON *.* TO 'cloud_admin'@'%';",
		"GRANT CONNECTION_ADMIN ON *.* TO 'cloud_admin'@'%';",

		"GRANT RESTRICTED_VARIABLES_ADMIN ON *.* TO 'cloud_admin'@'%';",
		"GRANT RESTRICTED_STATUS_ADMIN ON *.* TO 'cloud_admin'@'%';",
		"GRANT RESTRICTED_CONNECTION_ADMIN ON *.* TO 'cloud_admin'@'%';",
		"GRANT RESTRICTED_USER_ADMIN ON *.* TO 'cloud_admin'@'%';",
		"GRANT RESTRICTED_TABLES_ADMIN ON *.* TO 'cloud_admin'@'%';",
		"GRANT RESTRICTED_REPLICA_WRITER_ADMIN ON *.* TO 'cloud_admin'@'%';",

		"GRANT CREATE USER ON *.* TO 'cloud_admin'@'%';",
		"GRANT RELOAD ON *.* TO 'cloud_admin'@'%';",
		"GRANT PROCESS ON *.* TO 'cloud_admin'@'%';",
		"GRANT SELECT, INSERT, UPDATE, DELETE ON mysql.* TO 'cloud_admin'@'%';",
		"GRANT SELECT ON information_schema.* TO 'cloud_admin'@'%';",
		"GRANT SELECT ON performance_schema.* TO 'cloud_admin'@'%';",
		"GRANT SHOW DATABASES on *.* TO 'cloud_admin'@'%';",

		"GRANT SELECT ON *.* TO 'cloud_admin'@'%';",
		"GRANT INDEX ON *.* TO 'cloud_admin'@'%';",
		"GRANT INSERT ON *.* TO 'cloud_admin'@'%';",
		"GRANT UPDATE ON *.* TO 'cloud_admin'@'%';",
		"GRANT DELETE ON *.* TO 'cloud_admin'@'%';",
		"GRANT CREATE ON *.* TO 'cloud_admin'@'%';",
		"GRANT DROP ON *.* TO 'cloud_admin'@'%';",
		"GRANT ALTER ON *.* TO 'cloud_admin'@'%';",
		"GRANT CREATE VIEW ON *.* TO 'cloud_admin'@'%';",

		"GRANT SHUTDOWN, CONFIG ON *.* TO 'cloud_admin'@'%';",
		"REVOKE SHUTDOWN, CONFIG ON *.* FROM root;",
	}

	// Some historical old cluster has not enabled redact log
	enableRedactLog = `SET GLOBAL tidb_redact_log = 1;`
)
