package common

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

	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"golang.org/x/mod/semver"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	sdk "github.com/tidbcloud/infra-provider/pkg/gitops-sdk"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/util/collections"
)

const (
	initializedKey            = "eks.infra.pingcap.com/initialized"
	bootstrapSQLConfigMapName = "bootstrap-sql"

	initSQL = `SET GLOBAL tidb_redact_log = 1;`

	analyzeSQL = `SET GLOBAL tidb_analyze_version = 1;`

	setupBuiltinRoles = `
CREATE ROLE IF NOT EXISTS 'role_admin';
GRANT ALL ON *.* TO 'role_admin' WITH GRANT OPTION;
REVOKE SYSTEM_USER ON *.* FROM 'role_admin';
REVOKE SHUTDOWN, CONFIG ON *.* FROM 'role_admin';
GRANT ROLE_ADMIN ON *.* to 'role_admin';

# don't create role_developer for now
# CREATE ROLE IF NOT EXISTS 'role_developer';
# GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO 'role_developer';
# REVOKE SYSTEM_USER ON *.* FROM 'role_developer';
# REVOKE ALL ON mysql.* FROM 'role_developer';

# don't create role_observer for now
# CREATE ROLE IF NOT EXISTS 'role_observer';
# GRANT SELECT ON *.* TO 'role_observer';
# REVOKE SYSTEM_USER ON *.* FROM 'role_observer';
# REVOKE ALL ON mysql.* FROM 'role_observer';
`

	// NOTE: the root account will be locked after all initial tasks finished in core-svc
	setupUsers = `
CREATE USER IF NOT EXISTS 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 BACKUP_ADMIN, RESTORE_ADMIN ON *.* TO 'cloud_admin'@'%';
GRANT ROLE_ADMIN ON *.* to 'role_admin';
GRANT SYSTEM_USER ON *.* TO 'cloud_admin'@'%' WITH GRANT OPTION;

GRANT RELOAD ON *.* TO 'cloud_admin'@'%';
GRANT PROCESS ON *.* TO 'cloud_admin'@'%';
GRANT SHUTDOWN, CONFIG ON *.* TO 'cloud_admin'@'%';

GRANT CREATE USER, CREATE ROLE 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'@'%';

# TODO(chenfei): for compatibility. We can remove the following privileges after all DP service(import, cdc, dm, etc.)
# have finished migrate all features to based on the delegate_user feature.
# Cross region requires these permissions, See https://docs.pingcap.com/tidb/stable/troubleshoot-ticdc/#when-the-sink-of-the-replication-downstream-is-tidb-or-mysql-what-permissions-do-users-of-the-downstream-database-need for more detail
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 SHOW DATABASES on *.* TO 'cloud_admin'@'%';
# used by import
GRANT REFERENCES ON *.* TO 'cloud_admin'@'%';

REVOKE SHUTDOWN, CONFIG ON *.* FROM root;
`
)

// TiDBInitializer manages the way to initialize TiDB cluster.
// It can use either the TidbInitializer CR or the bootstrap SQL feature (supported in v6.5.1+).
type TiDBInitializer struct {
	*task.BaseTask
}

func NewTiDBInitializer(ctx *task.Context) *TiDBInitializer {
	return &TiDBInitializer{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *TiDBInitializer) Name() string {
	return "TiDBInitializer"
}

func (t *TiDBInitializer) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	if t.TCGetter == nil || t.TCGetter.IsNil() {
		return deploy.Fatal(fmt.Errorf("tidb cluster is not ready yet"))
	}

	// Always make sure to create the configmap if Spec.TiDB.BootstrapSQLConfigMapName is set
	// since we definitely need it although the cluster is already "initialized".
	if tidbInitialized(t.Cluster) && t.TCGetter.BootstrapSQLConfigMapName() == "" {
		return deploy.Done()
	}

	sql, err := t.getInitSQL(ctx)
	if err != nil {
		return deploy.Error(err)
	}
	// If bootstrap SQL is supported and enabled,
	// create a configmap to store the SQL for bootstrapping.
	if t.TCGetter.BootstrapSQLConfigMapName() != "" {
		if err := t.createBootstrapSQL(ctx, sql); err != nil {
			return deploy.Error(err)
		}
	} else {
		ti, mut := t.tidbInitializer(ctx, sql)
		if _, err := controllerutil.CreateOrUpdate(ctx, t.EnvProvider.GetClient(), ti, mut); err != nil {
			return deploy.Error(err)
		}
	}

	return deploy.Done()
}

func (t *TiDBInitializer) PostCreate(ctx context.Context) deploy.Result {
	if tidbInitialized(t.Cluster) {
		return deploy.Done()
	}
	if t.TCGetter == nil || t.TCGetter.IsNil() {
		return deploy.Fatal(fmt.Errorf("tidb cluster is not ready yet"))
	}
	remote := t.EnvProvider.GetClient()

	if t.TCGetter.BootstrapSQLConfigMapName() != "" {
		var cm corev1.ConfigMap
		if err := remote.Get(ctx, types.NamespacedName{
			Name:      t.Namer.BootstrapSQLConfigMapName(),
			Namespace: t.Namer.GetTiDBNamespace(),
		}, &cm); err != nil {
			return deploy.Error(err)
		}
	} else {
		var ti opv1alpha1.TidbInitializer
		key := types.NamespacedName{
			Name:      t.Namer.GetTiDBClusterName(),
			Namespace: t.Namer.GetTiDBNamespace(),
		}
		if err := remote.Get(ctx, key, &ti); err != nil {
			return deploy.Error(err)
		}
		if ti.Status.JobStatus.Succeeded == 0 {
			return deploy.Error(fmt.Errorf("initializer job failed"))
		}
	}

	t.Cluster.Annotations = collections.MergeMaps(
		t.Cluster.Annotations,
		map[string]string{initializedKey: "true"},
	)

	return deploy.Done()
}

func tidbInitialized(c *cloudv1.Cluster) bool {
	// TiDB would only be initialized once, so no operations needed once
	// it's annoatated as initialized
	return util.GetAnnotation(c, initializedKey) == "true"
}

// migrate from https://github.com/tidbcloud/infra-provider/blob/39426aa10d959aaf6e31d2e441a70f21efdd3de7/infra-api/controllers/cluster/cluster_creation.go#L323-L324
func experimentalSupported(expFeatures []config.ExpFeatureGate, curVersion string, tenantID, projectID string) string {
	supported := make([]string, 0, len(expFeatures))
	for _, expFeature := range expFeatures {
		if semver.Compare(curVersion, expFeature.MinSupportVersion) >= 0 &&
			config.IsFeatureEnabled(expFeature.FeatureGate, tenantID, projectID) {
			commands := strings.Split(expFeature.Command, ";")
			for _, cmd := range commands {
				trimed := strings.TrimSpace(cmd)
				if len(trimed) > 0 {
					supported = append(supported, trimed)
				}
			}
		}
	}
	return strings.Join(supported, ";\n") + ";\n"
}

func (t *TiDBInitializer) createBootstrapSQL(ctx context.Context, sql string) error {
	t.Logger.Info("use bootstrap sql to init tidb")

	cli := t.EnvProvider.GetClient()
	cm := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      t.Namer.BootstrapSQLConfigMapName(),
			Namespace: t.Namer.GetTiDBNamespace(),
		},
		Data: map[string]string{
			consts.BootstrapSQLCMKey: sql,
		},
	}
	if err := k8s.CreateIfNotExist(ctx, cli, cm); err != nil {
		t.Logger.Error(err, "failed to create bootstrap sql configmap")
		return err
	}

	return nil
}

// tidbInitializer returns the TidbInitializer CR and the mutator for it.
func (t *TiDBInitializer) tidbInitializer(_ context.Context, sql string) (ti *opv1alpha1.TidbInitializer, mut func() error) {
	cli := t.EnvProvider.GetClient()
	ti = NewTidbInitializerWithInitSQL(t.Namer.GetTiDBNamespace(), t.Namer.GetTiDBClusterName(), &sql)

	mut = func() error {
		ti.Spec.Clusters = opv1alpha1.TidbClusterRef{
			Name:      t.Namer.GetTiDBClusterName(),
			Namespace: t.Namer.GetTiDBNamespace(),
		}
		// Override the base images for private EKS clusters.
		ti.Spec.Image = sdk.OverrideBaseImage(t.Cluster, ti.Spec.Image)
		if err := controllerutil.SetOwnerReference(t.TCGetter.Object(), ti, cli.Scheme()); err != nil {
			return err
		}
		return nil
	}

	return ti, mut
}

func (t *TiDBInitializer) getInitSQL(ctx context.Context) (string, error) {
	var initsql string
	obj := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: t.Cluster.Namespace,
			Name:      namer.BootstrapSQLConfigMapNameInMeta(t.Cluster.GetClusterID()),
		},
	}
	// get initsql from controlplane configmap if it exists
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
		if !errors.IsNotFound(err) {
			return "", err

		}
		experimentalEnabling := experimentalSupported(t.Config.ExpFeatureGates, t.TCGetter.Version(), t.Cluster.GetTenantID(), t.Cluster.GetProjectID())

		initsql = BuildInitSql(true, true, experimentalEnabling)
	} else {
		initsql = obj.Data[consts.BootstrapSQLCMKey]
	}
	return initsql, nil
}

func NewTidbInitializerWithInitSQL(namespace, name string, initsql *string) *opv1alpha1.TidbInitializer {
	pullIfNotPresent := corev1.PullIfNotPresent
	return &opv1alpha1.TidbInitializer{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      name,
		},
		Spec: opv1alpha1.TidbInitializerSpec{
			Clusters:        opv1alpha1.TidbClusterRef{Namespace: namespace, Name: consts.ClusterPrefix},
			InitSql:         initsql,
			Image:           config.GetMysqlClientImage(),
			ImagePullPolicy: &pullIfNotPresent,
		},
	}
}

func BuildInitSql(enableSEM, enableAnalyze bool, experimentalEnabling string) string {
	// If tidb_analyze_version is set to 2 in experimentalEnabling, version 1 set by analyzeSQL will be overwritten.
	initsql := initSQL + "\n" + experimentalEnabling

	if enableSEM {
		initsql = setupBuiltinRoles + "\n" + setupUsers + "\n" + initsql
	}

	if enableAnalyze {
		initsql = analyzeSQL + "\n" + initsql
	}
	return initsql
}
