package main

import (
	"context"
	"fmt"
	"log/slog"
	"strings"

	versionpkg "github.com/hashicorp/go-version"
	"github.com/spf13/cobra"
	pb "github.com/tidbcloud/config-center/proto/config_center"
	"github.com/tidbcloud/config-center/templategen"
	"github.com/tidbcloud/config-center/templategen/premium"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/protobuf/types/known/structpb"
)

// dialConfigCenter dials the configured config-center endpoint over plaintext gRPC.
// Callers own the returned conn and must Close it.
func dialConfigCenter() (pb.ConfigCenterServiceClient, *grpc.ClientConn, error) {
	conn, err := grpc.NewClient(configCenterEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return nil, nil, fmt.Errorf("failed to dial config-center at %s: %w", configCenterEndpoint, err)
	}
	return pb.NewConfigCenterServiceClient(conn), conn, nil
}

func newMigrateNGBaseCmd() *cobra.Command {
	var version string
	var staging bool
	cmd := &cobra.Command{
		Use:   "ng-base",
		Short: "migrate nextgen basic data",
		Long:  `migrate nextgen basic data`,
		RunE: func(cmd *cobra.Command, args []string) error {
			client, conn, err := dialConfigCenter()
			if err != nil {
				return err
			}
			defer conn.Close()

			return importNextGenBaseData(client, version, staging)
		},
	}

	cmd.Flags().StringVar(&version, "version", "v8.5.4", "version of the cluster, also set as default version")
	cmd.Flags().BoolVar(&staging, "staging", false, "whether to use staging/dev-only defaults")

	return cmd
}

func newMigrateByocInitSqlCmd() *cobra.Command {
	var version string
	var baseTemplate int64
	var staging bool
	cmd := &cobra.Command{
		Use:   "byoc-init-sql",
		Short: "migrate byoc init sql data",
		Long:  `migrate byoc init sql data`,
		RunE: func(cmd *cobra.Command, args []string) error {
			client, conn, err := dialConfigCenter()
			if err != nil {
				return err
			}
			defer conn.Close()

			return importNextGenInitSql(client, version, true, baseTemplate, staging)
		},
	}

	cmd.Flags().StringVar(&version, "version", "v8.5.4", "version of the cluster")
	// default 0 means auto generate id
	cmd.Flags().Int64Var(&baseTemplate, "base-template-id", 0, "base template id for nextgen templates")
	cmd.Flags().BoolVar(&staging, "staging", false, "whether to use staging/dev-only defaults")

	return cmd
}

func newMigrateNGCDCCmd() *cobra.Command {
	var version string
	var staging bool
	cmd := &cobra.Command{
		Use:   "ng-cdc",
		Short: "migrate nextgen cdc data",
		Long:  `migrate nextgen cdc data`,
		RunE: func(cmd *cobra.Command, args []string) error {
			client, conn, err := dialConfigCenter()
			if err != nil {
				return err
			}
			defer conn.Close()

			return importNextGenCDCData(client, version, staging)
		},
	}

	cmd.Flags().StringVar(&version, "version", "v8.5.4", "version of the cluster")
	cmd.Flags().BoolVar(&staging, "staging", false, "whether to use staging/dev-only defaults")

	return cmd
}

// brConfigTypes are the BR (backup & restore) config types the generator owns.
var brConfigTypes = []string{
	pb.SHARED_POOL_CONFIG_TIBR_AUTOBACKUP,
	pb.SHARED_POOL_CONFIG_TIBR_RESTORE,
	pb.SHARED_POOL_CONFIG_TIBRGC_T2_DFSGC,
	pb.SHARED_POOL_CONFIG_TIBRGC_T3_ARCHIVE,
	pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE,
}

func newMigrateNGBRCmd() *cobra.Command {
	var version string
	cmd := &cobra.Command{
		Use:   "ng-br",
		Short: "migrate nextgen br (backup & restore) data",
		Long:  `migrate nextgen br (backup & restore) configuration templates and param dicts`,
		RunE: func(cmd *cobra.Command, args []string) error {
			client, conn, err := dialConfigCenter()
			if err != nil {
				return err
			}
			defer conn.Close()

			versionFor := premium.VersionFor(version)
			for _, ct := range brConfigTypes {
				t, err := premium.Generator.Generate(ct, versionFor(ct))
				if err != nil {
					return err
				}
				if err := createPremiumTemplate(client, t); err != nil {
					return err
				}
			}
			return importNextGenBRParamDicts(client, version)
		},
	}

	cmd.Flags().StringVar(&version, "version", "v8.5.4", "version of the cluster")

	return cmd
}

func newUpdateImageCmd() *cobra.Command {
	var clusterType string
	var componentName string
	var componentVersion string
	var baseImage string
	var tag string
	cmd := &cobra.Command{
		Use:   "update-image",
		Short: "update a single component image",
		Long:  `Update the image tag for a single component. Specify cluster type, component name, version, base image, and tag.`,
		RunE: func(cmd *cobra.Command, args []string) error {
			ct, ok := pb.ClusterType_value[clusterType]
			if !ok {
				return fmt.Errorf("invalid cluster type %q, valid values: %v", clusterType, pb.ClusterType_name)
			}

			client, conn, err := dialConfigCenter()
			if err != nil {
				return err
			}
			defer conn.Close()

			slog.Info("Updating component image",
				"cluster_type", clusterType,
				"component", componentName,
				"version", componentVersion,
				"base_image", baseImage,
				"tag", tag,
			)
			_, err = client.UpdateComponentImage(context.Background(), &pb.UpdateComponentImageRequest{
				ClusterType:      pb.ClusterType(ct),
				ComponentName:    componentName,
				ComponentVersion: componentVersion,
				BaseImage:        baseImage,
				Tag:              tag,
			})
			if err != nil {
				return fmt.Errorf("failed to update component image: %w", err)
			}
			slog.Info("Component image updated successfully")
			return nil
		},
	}

	cmd.Flags().StringVar(&clusterType, "cluster-type", "SHARED_POOL", "cluster type (e.g. SHARED_POOL, LOGICAL_CLUSTER, TICDC_NG)")
	cmd.Flags().StringVar(&componentName, "component", "", "component name (e.g. pd, tikv, tidb, tiproxy, resource_manager, ...)")
	cmd.Flags().StringVar(&componentVersion, "version", "", "component version (e.g. v8.5.4)")
	cmd.Flags().StringVar(&baseImage, "base-image", "", "base image path (e.g. gcr.io/pingcap-public/dbaas/pd)")
	cmd.Flags().StringVar(&tag, "tag", "", "image tag")
	_ = cmd.MarkFlagRequired("component")
	_ = cmd.MarkFlagRequired("version")
	_ = cmd.MarkFlagRequired("base-image")
	_ = cmd.MarkFlagRequired("tag")

	return cmd
}

func importNextGenBaseData(c pb.ConfigCenterServiceClient, version string, staging bool) error {
	templates, err := premium.Generator.GenerateAll(premium.VersionFor(version))
	if err != nil {
		return err
	}
	for _, t := range templates {
		if err := createPremiumTemplate(c, t); err != nil {
			return err
		}
	}

	if err := importNextGenInitSql(c, version, false, 0, staging); err != nil {
		return err
	}

	if err := importNextGenInitSql(c, version, true, 0, staging); err != nil {
		return err
	}
	// BR templates (incl. SEM) are produced by the generator above; only the BR
	// param dicts still need the control plane to reconcile.
	if err := importNextGenBRParamDicts(c, version); err != nil {
		return err
	}

	// NOTE: which version is the cluster default is a separate operational
	// decision, set explicitly via the `version update` command — provisioning
	// templates for a version intentionally does NOT change the default. e.g.:
	//   ctl version update <version>      -T SHARED_POOL
	//   ctl version update v1.4.0         -T SHARED_POOL    --component tiproxy
	//   ctl version update v1.4.0         -T LOGICAL_CLUSTER --component tiproxy
	return nil
}

// createPremiumTemplate creates one generated template (its Data may be empty)
// and installs its default bindings at the template's version.
func createPremiumTemplate(c pb.ConfigCenterServiceClient, t templategen.Template) error {
	created, err := c.CreateConfigurationTemplate(context.Background(), &pb.CreateConfigurationTemplateRequest{
		ClusterType:       t.CreateClusterType,
		ConfigurationType: t.ConfigType,
		TemplateName:      t.Name,
		Data:              t.Data,
	})
	if err != nil {
		return fmt.Errorf("create premium template %s: %w", t.ConfigType, err)
	}
	for _, b := range t.Bindings {
		if _, err := c.UpdateDefaultConfigurationTemplate(context.Background(), &pb.UpdateDefaultConfigurationTemplateRequest{
			ClusterType:       b.ClusterType,
			ConfigurationType: b.ConfigType,
			Version:           t.Version,
			TemplateId:        created.TemplateId,
		}); err != nil {
			return fmt.Errorf("set default premium template %s/%s: %w", b.ClusterType, b.ConfigType, err)
		}
	}
	return nil
}

func importNextGenInitSql(c pb.ConfigCenterServiceClient, version string, byoc bool, templateID int64, staging bool) error {
	initsqlStep1 := &pb.ParamItem{
		Value: structpb.NewStringValue(`
REVOKE SHUTDOWN, CONFIG ON *.* FROM root;
CREATE USER IF NOT EXISTS platform_admin;
GRANT SYSTEM_VARIABLES_ADMIN ON *.* TO 'platform_admin'@'%';
GRANT RESTRICTED_VARIABLES_ADMIN ON *.* TO 'platform_admin'@'%';
GRANT RESTRICTED_TABLES_ADMIN ON *.* TO 'platform_admin'@'%';
GRANT RESTRICTED_USER_ADMIN ON *.* TO 'platform_admin'@'%';
GRANT RESTRICTED_AUDIT_ADMIN ON *.* TO 'platform_admin'@'%';
GRANT RELOAD ON *.* TO 'platform_admin'@'%';
GRANT SYSTEM_USER ON *.* TO 'platform_admin'@'%' WITH GRANT OPTION;
`),
		Comment: "step1: create platform_admin user",
	}
	initsqlStep2 := &pb.ParamItem{
		Value: structpb.NewStringValue(`
CREATE USER IF NOT EXISTS maintenance_admin;
GRANT SYSTEM_VARIABLES_ADMIN ON *.* TO 'maintenance_admin'@'%';
GRANT RESTRICTED_VARIABLES_ADMIN ON *.* TO 'maintenance_admin'@'%';
GRANT RESTRICTED_TABLES_ADMIN ON *.* TO 'maintenance_admin'@'%';
GRANT RESTRICTED_USER_ADMIN ON *.* TO 'maintenance_admin'@'%';
GRANT RELOAD ON *.* TO 'maintenance_admin'@'%';
GRANT SYSTEM_USER ON *.* TO 'maintenance_admin'@'%' WITH GRANT OPTION;
GRANT CONFIG ON *.* TO 'maintenance_admin'@'%';
`),
		Comment: "step2: create maintenance_admin user",
	}

	initsqlStep3 := &pb.ParamItem{
		Value: structpb.NewStringValue(`
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 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'@'%';

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'@'%';

GRANT REFERENCES ON *.* TO 'cloud_admin'@'%';
`),
		Comment: "step3: create cloud_admin user",
	}

	initsqlStep4SQL := `
SET GLOBAL tidb_redact_log = 1;
SET GLOBAL tidb_analyze_version = 2;
SET GLOBAL tidb_runtime_filter_mode = LOCAL;
`
	if premium.IsV263(version) {
		initsqlStep4SQL += "SET GLOBAL tidb_stmt_summary_refresh_interval = 300;\n"
		initsqlStep4SQL += "SET GLOBAL tidb_stmt_summary_group_by_user = 1;\n"
	}
	initsqlStep4 := &pb.ParamItem{
		Value:   structpb.NewStringValue(initsqlStep4SQL),
		Comment: "step4: set global variables",
	}

	initsqlStepBYOC := &pb.ParamItem{
		Value: structpb.NewStringValue(`
DO audit_log_create_filter('all', '{}');
DO audit_log_create_rule('platform_admin@%', 'all');
DO audit_log_create_rule('maintenance_admin@%', 'all');
`),
		Comment: "step5: for BYOC",
	}

	initsqlReq := &pb.CreateConfigurationTemplateRequest{
		ClusterType:       pb.ClusterType_SHARED_POOL,
		ConfigurationType: pb.SHARED_POOL_CONFIG_INITSQL,
		TemplateName:      "system template of premium initsql",

		Data: map[string]*pb.ParamItem{
			"100": initsqlStep1,
			"200": initsqlStep2,
			"400": initsqlStep4,
		},
	}
	if !staging && !byoc {
		initsqlReq.Data["300"] = initsqlStep3
	}
	if byoc {
		initsqlReq.Data["500"] = initsqlStepBYOC
	}
	if templateID > 0 {
		initsqlReq.TemplateId = &templateID
	}

	initsqlTemplate, err := c.CreateConfigurationTemplate(context.Background(), initsqlReq)
	if err != nil {
		return err
	}

	defaultReq := &pb.UpdateDefaultConfigurationTemplateRequest{
		ClusterType:       pb.ClusterType_SHARED_POOL,
		ConfigurationType: pb.SHARED_POOL_CONFIG_INITSQL,
		Version:           version,
		TemplateId:        initsqlTemplate.TemplateId,
	}
	if byoc {
		defaultReq.Regions = []string{pb.BYOC_EXAMPLE_REGION}
		binds, err := c.ListConfigurationTemplateDefaultCaseBindings(context.Background(), &pb.ListConfigurationTemplateDefaultCaseBindingsRequest{
			ClusterType:       pb.ClusterType_SHARED_POOL,
			ConfigurationType: pb.SHARED_POOL_CONFIG_INITSQL,
			Version:           version,
		})
		if err != nil {
			return err
		}
		for _, bind := range binds.GetTemplateDefaultCaseBindings() {
			// FIXME: detect BYOC region more precisely
			if strings.Contains(bind.GetDefaultCase().GetRegion(), "byoc") || bind.GetDefaultCase().GetRegion() == "eu-central-1-marat" {
				defaultReq.Regions = append(defaultReq.Regions, bind.GetDefaultCase().GetRegion())
			}
		}
	}
	_, err = c.UpdateDefaultConfigurationTemplate(context.Background(), defaultReq)
	if err != nil {
		return err
	}

	return nil
}

func importNextGenCDCData(c pb.ConfigCenterServiceClient, version string, staging bool) error {
	ticdcTemplate, err := c.CreateConfigurationTemplate(context.Background(), &pb.CreateConfigurationTemplateRequest{
		ClusterType:       pb.ClusterType_TICDC_NG,
		ConfigurationType: pb.TICDC_CONFIG_TICDC,
		TemplateName:      "system template of premium ticdc",
		Data: tomlConfig2Params(`
newarch = true
[debug]
enable-remote-event-service = false
`),
	})
	if err != nil {
		return err
	}
	_, err = c.UpdateDefaultConfigurationTemplate(context.Background(), &pb.UpdateDefaultConfigurationTemplateRequest{
		ClusterType:       pb.ClusterType_TICDC_NG,
		ConfigurationType: pb.TICDC_CONFIG_TICDC,
		Version:           version,
		TemplateId:        ticdcTemplate.TemplateId,
	})
	if err != nil {
		return err
	}

	for _, ctype := range pb.ClusterType_CHANGEFEED.AvailableConfigTypes() {
		resp, err := c.UpdateDefaultConfigurationTemplate(context.Background(), &pb.UpdateDefaultConfigurationTemplateRequest{
			ClusterType:       pb.ClusterType_CHANGEFEED_NG,
			ConfigurationType: ctype,
			Version:           "",
			CloudProvider:     "",
			NodeInstance:      "",
			TemplateId:        0,
		})
		if err != nil {
			panic(err)
		}
		slog.Info("imported default template", "cluster_type", "CHANGEFEED_NG", "config_type", ctype, "template_id", resp.TemplateId)
		resp, err = c.UpdateDefaultConfigurationTemplate(context.Background(), &pb.UpdateDefaultConfigurationTemplateRequest{
			ClusterType:       pb.ClusterType_CHANGEFEED_ESSENTIAL,
			ConfigurationType: ctype,
			Version:           "",
			CloudProvider:     "",
			NodeInstance:      "",
			TemplateId:        0,
		})
		if err != nil {
			panic(err)
		}
		slog.Info("imported default template", "cluster_type", "CHANGEFEED_ESSENTIAL", "config_type", ctype, "template_id", resp.TemplateId)
	}

	return nil
}

// importNextGenBRParamDicts reconciles the BR component param dicts (the
// Configurable parameters surfaced in the UI). BR templates are produced by the
// generator; component images are managed out of band.
func importNextGenBRParamDicts(c pb.ConfigCenterServiceClient, version string) error {
	clusterVersion := version
	parsedVersion, err := versionpkg.NewVersion(clusterVersion)
	if err != nil {
		return fmt.Errorf("invalid BR param dict version %q: %w", clusterVersion, err)
	}
	segments := parsedVersion.Segments()
	version = fmt.Sprintf("v%d.%d", segments[0], segments[1])

	slog.Info("Creating BR param dicts", "cluster_version", clusterVersion, "version", version)

	type dictEntry struct {
		configType string
		items      []*pb.ConfigurationDictItem
	}

	brDictEntries := []dictEntry{
		{
			configType: pb.SHARED_POOL_CONFIG_TIBR_AUTOBACKUP,
			items: []*pb.ConfigurationDictItem{
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_AUTOBACKUP, version, "native-br.backup-tolerate-err", pb.ConfigParamType_bool, structpb.NewBoolValue(true),
					"Whether to tolerate single store unavailability and continue backup"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_AUTOBACKUP, version, "native-br.backup-delay", pb.ConfigParamType_string, structpb.NewStringValue("5s"),
					"Pre-backup delay, also controls backup_ts propagation to rfengine"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_AUTOBACKUP, version, "native-br.backup-ts-wait-timeout", pb.ConfigParamType_string, structpb.NewStringValue("30s"),
					"Max wait time for obtaining backup timestamp"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_AUTOBACKUP, version, "native-br.backup-ts-ttl", pb.ConfigParamType_string, structpb.NewStringValue("60s"),
					"Backup timestamp TTL, must be >= backup-ts-wait-timeout"),
			},
		},
		{
			configType: pb.SHARED_POOL_CONFIG_TIBR_RESTORE,
			items: []*pb.ConfigurationDictItem{
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_RESTORE, version, "native-br.restore-task-ttl", pb.ConfigParamType_string, structpb.NewStringValue("60m"),
					"Survival time after restore task reaches terminal state"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_RESTORE, version, "native-br.restore-timeout-wait-flush", pb.ConfigParamType_string, structpb.NewStringValue("10m"),
					"Timeout for waiting mem-table flush to disk"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_RESTORE, version, "native-br.restore-timeout-restore-snapshot", pb.ConfigParamType_string, structpb.NewStringValue("10m"),
					"Request timeout for restoring snapshot to TiKV"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_RESTORE, version, "native-br.restore-timeout-fetch-wal", pb.ConfigParamType_string, structpb.NewStringValue("10m"),
					"Timeout for fetching latest WAL chunk"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_RESTORE, version, "native-br.restore-timeout-pd-control", pb.ConfigParamType_string, structpb.NewStringValue("10s"),
					"Timeout for PD control operations"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_RESTORE, version, "native-br.restore-max-retry", pb.ConfigParamType_int, structpb.NewNumberValue(30),
					"Max retries from region split to snapshot restore"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_RESTORE, version, "native-br.restore-coarse-split-regions-factor", pb.ConfigParamType_int, structpb.NewNumberValue(64),
					"Region split granularity factor during restore"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_RESTORE, version, "native-br.restore-tolerate-err", pb.ConfigParamType_bool, structpb.NewBoolValue(false),
					"Whether to tolerate single store unavailability"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_RESTORE, version, "native-br.lower-memory", pb.ConfigParamType_bool, structpb.NewBoolValue(true),
					"Low memory mode for restore"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBR_RESTORE, version, "native-br.instant-backup-timeout", pb.ConfigParamType_string, structpb.NewStringValue("60s"),
					"Max time for instant backup before restore"),
			},
		},
		{
			configType: pb.SHARED_POOL_CONFIG_TIBRGC_T2_DFSGC,
			items: []*pb.ConfigurationDictItem{
				// concurrency default is computed by control plane, so no default_value
				brDictItem(pb.SHARED_POOL_CONFIG_TIBRGC_T2_DFSGC, version, "concurrency", pb.ConfigParamType_int, nil,
					"S3 request concurrency (computed by control plane based on resource pool size)"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBRGC_T2_DFSGC, version, "file-types-whitelist", pb.ConfigParamType_string, structpb.NewStringValue("sst"),
					"File type whitelist, comma-separated"),
			},
		},
		{
			configType: pb.SHARED_POOL_CONFIG_TIBRGC_T3_ARCHIVE,
			items: []*pb.ConfigurationDictItem{
				brDictItem(pb.SHARED_POOL_CONFIG_TIBRGC_T3_ARCHIVE, version, "concurrency", pb.ConfigParamType_int, structpb.NewNumberValue(64),
					"S3 request concurrency"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBRGC_T3_ARCHIVE, version, "fetch-wal-timeout", pb.ConfigParamType_string, structpb.NewStringValue("10m"),
					"Timeout for fetching WAL chunks"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBRGC_T3_ARCHIVE, version, "max-archive-file-size", pb.ConfigParamType_int, structpb.NewNumberValue(1073741824),
					"Max size per archive package file (bytes)"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBRGC_T3_ARCHIVE, version, "skip-abnormal-shards", pb.ConfigParamType_string, structpb.NewStringValue(""),
					"Skip abnormal shards (comma-separated shard IDs)"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBRGC_T3_ARCHIVE, version, "skip-no-meta-days", pb.ConfigParamType_int, structpb.NewNumberValue(0),
					"Skip days without backup meta"),
				brDictItem(pb.SHARED_POOL_CONFIG_TIBRGC_T3_ARCHIVE, version, "store-concurrency", pb.ConfigParamType_int, structpb.NewNumberValue(3),
					"TiKV store restore concurrency"),
			},
		},
		{
			configType: pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE,
			items: []*pb.ConfigurationDictItem{
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "with-sys-table", pb.ConfigParamType_bool, structpb.NewBoolValue(true),
					"Whether to restore system privilege tables"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "check-requirements", pb.ConfigParamType_bool, structpb.NewBoolValue(false),
					"Pre-execution version check"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "checksum", pb.ConfigParamType_bool, structpb.NewBoolValue(false),
					"Run checksum at end of task"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "load-stats", pb.ConfigParamType_bool, structpb.NewBoolValue(true),
					"Load statistics or trigger auto-analyze"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "checksum-concurrency", pb.ConfigParamType_int, structpb.NewNumberValue(4),
					"Checksum concurrency"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "stats-concurrency", pb.ConfigParamType_int, structpb.NewNumberValue(12),
					"Stats restore concurrency"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "fast-load-sys-tables", pb.ConfigParamType_bool, structpb.NewBoolValue(false),
					"Fast system table loading via renaming"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "online", pb.ConfigParamType_bool, structpb.NewBoolValue(false),
					"(Experimental) Whether to stay online during restore"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "tikv-max-restore-concurrency", pb.ConfigParamType_int, structpb.NewNumberValue(4),
					"Thread pool size for task execution per store"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "ratelimit", pb.ConfigParamType_int, structpb.NewNumberValue(50),
					"Restore rate limit in MiB/s"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "sys-check-collation", pb.ConfigParamType_bool, structpb.NewBoolValue(true),
					"Whether to check collation compatibility for system tables"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "wait-tiflash-ready", pb.ConfigParamType_bool, structpb.NewBoolValue(false),
					"Whether to wait for TiFlash replicas ready"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "with-tidb-placement-mode", pb.ConfigParamType_string, structpb.NewStringValue("STRICT"),
					"Maps to tidb variable with-tidb-placement-mode"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "merge-region-size-bytes", pb.ConfigParamType_int, structpb.NewNumberValue(2359296000),
					"Threshold for merging small regions (bytes)"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "merge-region-key-count", pb.ConfigParamType_int, structpb.NewNumberValue(225000000),
					"Key count threshold for merging small regions"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "batch-flush-interval", pb.ConfigParamType_string, structpb.NewStringValue("16s"),
					"Auto-send interval for restore batches"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "ddl-batch-size", pb.ConfigParamType_int, structpb.NewNumberValue(128),
					"DDL batch table creation size"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "log-level", pb.ConfigParamType_string, structpb.NewStringValue("info"),
					"Log level"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "redact-info-log", pb.ConfigParamType_bool, structpb.NewBoolValue(false),
					"Whether to redact sensitive information in logs"),
				brDictItem(pb.SHARED_POOL_CONFIG_CLASSIC_RESTORE, version, "switch-mode-interval", pb.ConfigParamType_string, structpb.NewStringValue("5m"),
					"Interval for maintaining TiKV import mode during restore"),
			},
		},
	}

	for _, de := range brDictEntries {
		slog.Info("Processing param dict", "config_type", de.configType, "items", len(de.items))

		// List existing dict items for this config type + version to make the operation idempotent
		configType := de.configType
		existingResp, err := c.ListConfigurationDictItems(context.Background(), &pb.ListConfigurationDictItemsRequest{
			ClusterType:       pb.ClusterType_SHARED_POOL,
			ConfigurationType: &configType,
			Version:           &version,
			PageSize:          1000,
		})
		if err != nil {
			return fmt.Errorf("failed to list existing dict items for %s: %w", de.configType, err)
		}

		existingKeys := make(map[string]*pb.ConfigurationDictItem)
		for _, item := range existingResp.GetConfigurationDictItems() {
			existingKeys[item.GetParamKey()] = item
		}

		var toCreate []*pb.ConfigurationDictItem
		for _, item := range de.items {
			if existing, ok := existingKeys[item.GetParamKey()]; ok {
				// Item exists — update if values differ
				if !valuesEqual(existing.GetDefaultValue(), item.GetDefaultValue()) || existing.GetType() != item.GetType() {
					slog.Info("Updating existing dict item", "config_type", de.configType, "param_key", item.GetParamKey())
					_, err := c.UpdateConfigurationDictItem(context.Background(), &pb.UpdateConfigurationDictItemReq{
						ClusterType:       pb.ClusterType_SHARED_POOL,
						ConfigurationType: de.configType,
						Version:           version,
						ParamKey:          item.GetParamKey(),
						DefaultValue:      item.GetDefaultValue(),
						Description:       item.Description,
					})
					if err != nil {
						return fmt.Errorf("failed to update dict item %s/%s: %w", de.configType, item.GetParamKey(), err)
					}
				} else {
					slog.Info("Dict item already exists with matching values, skipping", "config_type", de.configType, "param_key", item.GetParamKey())
				}
			} else {
				toCreate = append(toCreate, item)
			}
		}

		if len(toCreate) > 0 {
			slog.Info("Creating new param dict items", "config_type", de.configType, "count", len(toCreate))
			_, err := c.BatchCreateConfigurationDictItems(context.Background(), &pb.BatchCreateConfigurationDictRequest{
				ConfigurationDictItems: toCreate,
			})
			if err != nil {
				return fmt.Errorf("failed to create param dict for %s: %w", de.configType, err)
			}
		}

		// Detect stale dict items that exist in DB but are no longer defined in code
		desiredKeys := make(map[string]struct{})
		for _, item := range de.items {
			desiredKeys[item.GetParamKey()] = struct{}{}
		}
		for key := range existingKeys {
			if _, ok := desiredKeys[key]; !ok {
				slog.Warn("Stale dict item found: exists in DB but not in code, consider manual removal",
					"config_type", de.configType, "param_key", key, "version", version)
			}
		}
	}

	slog.Info("NextGen BR migration completed successfully")
	return nil
}

// brDictItem creates a ConfigurationDictItem for a BR component parameter.
// defaultValue can be nil for parameters whose default is computed dynamically by the control plane.
func brDictItem(configType, version, paramKey string, paramType pb.ConfigParamType, defaultValue *structpb.Value, description string) *pb.ConfigurationDictItem {
	desc := description
	return &pb.ConfigurationDictItem{
		ClusterType:       pb.ClusterType_SHARED_POOL,
		ConfigurationType: configType,
		Version:           version,
		ParamKey:          paramKey,
		DefaultValue:      defaultValue,
		Type:              paramType,
		Description:       &desc,
	}
}

// valuesEqual compares two structpb.Value objects
func valuesEqual(v1, v2 *structpb.Value) bool {
	if v1 == nil && v2 == nil {
		return true
	}
	if v1 == nil || v2 == nil {
		return false
	}

	// Use AsInterface for comparison as it handles all value types
	i1 := v1.AsInterface()
	i2 := v2.AsInterface()

	// Simple comparison works for basic types (string, number, bool)
	// For complex types, you might need deeper comparison
	return fmt.Sprintf("%v", i1) == fmt.Sprintf("%v", i2)
}
