package deploy

import (
	"context"
	"fmt"
	"strings"

	"github.com/go-gormigrate/gormigrate/v2"
	"github.com/samber/lo"
	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/clusterconfig"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	configcenter "github.com/tidbcloud/config-center/proto/config_center"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
	"gorm.io/gorm"
)

func init() {
	registerMigrateRecord((*migrator).migrate20260203)
}

// migrate20260203 backfills tidb/tiproxy components and config templates for existing sharedpools,
// and creates tidb-default config for existing tidb clusters.
// This supports the multi-TiDB Group feature where:
// - SharedPool manages tidb/tiproxy version and config templates
// - TiDBCluster derives tidb/tiproxy from sharedpool and creates tidb-default
// - TiDBGroup derives tidb-{group} from tidb-default
func (m *migrator) migrate20260203(ctx context.Context) gormigrate.MigrateFunc {
	return func(db *gorm.DB) error {
		return InvokeRunMigrateWithRetry(ctx, func() error {
			return db.Transaction(func(tx *gorm.DB) error {
				// Step 1: Backfill sharedpools with tidb/tiproxy components and config templates
				if err := backfillSharedPoolComponents(ctx, m.sharedPoolRepo, m.clusterConfig); err != nil {
					return fmt.Errorf("failed to backfill sharedpool components: %w", err)
				}

				// Step 2: Backfill tidb clusters with tidb-default config
				if err := backfillTiDBClusterDefaultConfig(ctx, m.sharedPoolRepo, m.tidbClusterRepo, m.tidbGroupRepo, m.clusterConfig); err != nil {
					return fmt.Errorf("failed to backfill tidb cluster default config: %w", err)
				}

				tilog.Info(ctx, "migration 20260203 completed successfully")
				return nil
			})
		})
	}
}

// backfillSharedPoolComponents adds tidb/tiproxy components and config templates to existing sharedpools
func backfillSharedPoolComponents(ctx context.Context, sharedPoolRepo repo.SharedPoolRepo, clusterConfig clusterconfig.Client) error {
	sharedPools, err := sharedPoolRepo.List(ctx, &repo.SharedPoolFilter{Deleted: false})
	if err != nil {
		return fmt.Errorf("failed to list shared pools: %w", err)
	}

	if len(sharedPools) == 0 {
		tilog.PlainInfo("no shared pools found, skip backfill sharedpool components")
		return nil
	}

	tilog.Info(ctx, "start to backfill tidb/tiproxy components for sharedpools", zap.Int("sharedPoolCount", len(sharedPools)))

	for _, sharedPool := range sharedPools {
		if err := backfillSingleSharedPool(ctx, sharedPool, clusterConfig); err != nil {
			tilog.Error(ctx, "failed to backfill sharedpool, will retry", zap.String("sharedPoolId", sharedPool.GetId()), zap.Error(err))
			return err
		}
	}

	tilog.Info(ctx, "completed backfilling tidb/tiproxy components for sharedpools", zap.Int("sharedPoolCount", len(sharedPools)))
	return nil
}

func backfillSingleSharedPool(ctx context.Context, sharedPool *entity.SharedPool, clusterConfig clusterconfig.Client) error {
	sharedPoolId := sharedPool.GetId()

	// Get tenant ID from entity.SharedPool - it has TenantId field directly
	tenantIdStr := sharedPool.TenantId
	tenantId, err := clusterconfig.ParseTenantId(tenantIdStr, true)
	if err != nil {
		return fmt.Errorf("failed to parse tenant id for sharedPool %s: %w", sharedPoolId, err)
	}

	// Get existing sharedpool meta from config center to check if tidb/tiproxy already exists
	client := clusterConfig.GetClient()
	metaResp, err := client.GetClusterMeta(ctx, &configcenter.GetClusterMetaRequest{
		ClusterType: configcenter.ClusterType_SHARED_POOL,
		ClusterId:   sharedPoolId,
	})
	if err != nil {
		return fmt.Errorf("get sharedpool meta for sharedPool %s: %w", sharedPoolId, err)
	}

	// Check if tidb/tiproxy components already exist
	hasTiDB := false
	hasTiProxy := false
	tidbComponentName := strings.ToLower(apiv1.ComponentType_TIDB.String())
	tiproxyComponentName := strings.ToLower(apiv1.ComponentType_TIPROXY.String())

	for _, comp := range metaResp.GetComponents() {
		if comp.GetComponentName() == tidbComponentName {
			hasTiDB = true
		}
		if comp.GetComponentName() == tiproxyComponentName {
			hasTiProxy = true
		}
	}

	if hasTiDB && hasTiProxy {
		tilog.Info(ctx, "sharedpool already has tidb/tiproxy components, skip backfill", zap.String("sharedPoolId", sharedPoolId))
		return nil
	}

	// TiDB version is the same as sharedpool version
	tidbVersion := sharedPool.GetVersion()
	if tidbVersion == "" {
		return fmt.Errorf("sharedpool version is empty for sharedPool %s", sharedPoolId)
	}

	// Get default tiproxy version for this tenant (tiproxy has independent versioning)
	tiproxyVersion, err := getTiProxyDefaultVersion(ctx, client, tenantId)
	if err != nil {
		return fmt.Errorf("get tiproxy default version for sharedPool %s: %w", sharedPoolId, err)
	}

	tilog.Info(ctx, "backfilling tidb/tiproxy components for sharedpool",
		zap.String("sharedPoolId", sharedPoolId),
		zap.String("tidbVersion", tidbVersion),
		zap.String("tiproxyVersion", tiproxyVersion),
		zap.Bool("hasTiDB", hasTiDB),
		zap.Bool("hasTiProxy", hasTiProxy),
	)

	// Build AutoUpdateClusterMeta request to add both components and config templates
	// Using AutoUpdateClusterConfigurationFileRequest with Version field ensures config center
	// populates the configuration from system templates
	metaReq := &configcenter.AutoUpdateClusterMetaRequest{
		ClusterType:               configcenter.ClusterType_SHARED_POOL,
		ClusterId:                 sharedPoolId,
		ComponentRequests:         make([]*configcenter.UpdateClusterComponentRequest, 0, 2),
		ConfigurationFileRequests: make([]*configcenter.AutoUpdateClusterConfigurationFileRequest, 0, 2),
	}

	if !hasTiDB {
		metaReq.ComponentRequests = append(metaReq.ComponentRequests, &configcenter.UpdateClusterComponentRequest{
			ClusterType:       configcenter.ClusterType_SHARED_POOL,
			ClusterId:         sharedPoolId,
			ComponentName:     tidbComponentName,
			OverwriteImageTag: proto.String(""),
			Version:           lo.ToPtr(tidbVersion),
			AllowMissing:      true,
		})
		// Config template with Version - config center will populate from system template
		metaReq.ConfigurationFileRequests = append(metaReq.ConfigurationFileRequests,
			&configcenter.AutoUpdateClusterConfigurationFileRequest{
				ClusterType:           configcenter.ClusterType_SHARED_POOL,
				ClusterId:             sharedPoolId,
				ConfigurationFilename: tidbComponentName,
				ConfigurationType:     proto.String(tidbComponentName),
				BelongToComponent:     proto.String(tidbComponentName),
				Format:                configcenter.Format_TOML.Enum(),
				Version:               tidbVersion,
			})
	}

	if !hasTiProxy {
		metaReq.ComponentRequests = append(metaReq.ComponentRequests, &configcenter.UpdateClusterComponentRequest{
			ClusterType:       configcenter.ClusterType_SHARED_POOL,
			ClusterId:         sharedPoolId,
			ComponentName:     tiproxyComponentName,
			OverwriteImageTag: proto.String(""),
			Version:           lo.ToPtr(tiproxyVersion),
			AllowMissing:      true,
		})
		// Config template with Version - config center will populate from system template
		metaReq.ConfigurationFileRequests = append(metaReq.ConfigurationFileRequests,
			&configcenter.AutoUpdateClusterConfigurationFileRequest{
				ClusterType:           configcenter.ClusterType_SHARED_POOL,
				ClusterId:             sharedPoolId,
				ConfigurationFilename: tiproxyComponentName,
				ConfigurationType:     proto.String(tiproxyComponentName),
				BelongToComponent:     proto.String(tiproxyComponentName),
				Format:                configcenter.Format_TOML.Enum(),
				Version:               tiproxyVersion,
			})
	}

	// Use AutoUpdateClusterMeta to add both components and config templates atomically
	if len(metaReq.ComponentRequests) > 0 || len(metaReq.ConfigurationFileRequests) > 0 {
		_, err = client.AutoUpdateClusterMeta(ctx, metaReq)
		if err != nil {
			return fmt.Errorf("auto update cluster meta for components and configs: %w", err)
		}
	}

	tilog.Info(ctx, "completed backfilling tidb/tiproxy for sharedpool", zap.String("sharedPoolId", sharedPoolId))
	return nil
}

// getTiProxyDefaultVersion gets the default tiproxy version for migration
// TiProxy has independent versioning separate from TiDB
func getTiProxyDefaultVersion(ctx context.Context, client configcenter.ConfigCenterServiceClient, tenantId *uint64) (string, error) {
	resp, err := client.GetDefaultVersion(ctx, &configcenter.GetDefaultVersionRequest{
		ClusterType: configcenter.ClusterType_SHARED_POOL,
		Component:   strings.ToLower(apiv1.ComponentType_TIPROXY.String()),
		TenantId:    tenantId,
	})
	if err != nil {
		return "", fmt.Errorf("get default tiproxy version: %w", err)
	}
	if resp.Version == "" {
		return "", fmt.Errorf("default tiproxy version is empty")
	}
	return resp.Version, nil
}

// backfillTiDBClusterDefaultConfig creates tidb-default config for existing tidb clusters
func backfillTiDBClusterDefaultConfig(ctx context.Context, sharedPoolRepo repo.SharedPoolRepo, tidbClusterRepo repo.TiDBClusterRepo, tidbGroupRepo repo.TiDBGroupRepo, clusterConfig clusterconfig.Client) error {
	// First get all shared pools, then list tidb clusters by SharedPoolIds
	// This is required because TiDBClusterListFilter requires at least one filter (Ids, TenantIds, or SharedPoolIds)
	sharedPools, err := sharedPoolRepo.List(ctx, &repo.SharedPoolFilter{Deleted: false})
	if err != nil {
		return fmt.Errorf("failed to list shared pools: %w", err)
	}

	if len(sharedPools) == 0 {
		tilog.PlainInfo("no shared pools found, skip backfill tidb-default config")
		return nil
	}

	totalCount := 0
	for _, sharedPool := range sharedPools {
		tidbClusters, err := tidbClusterRepo.List(ctx, &repo.TiDBClusterListFilter{
			SharedPoolIds: []string{sharedPool.GetId()},
			Deleted:       false,
		})
		if err != nil {
			return fmt.Errorf("failed to list tidb clusters for sharedpool %s: %w", sharedPool.GetId(), err)
		}

		for _, tidbCluster := range tidbClusters {
			if err := backfillSingleTiDBClusterDefaultConfig(ctx, tidbCluster, tidbGroupRepo, clusterConfig); err != nil {
				tilog.Error(ctx, "failed to backfill tidb cluster default config, will retry", zap.String("tidbClusterId", tidbCluster.GetId()), zap.Error(err))
				return err
			}
			totalCount++
		}
	}

	tilog.Info(ctx, "completed backfilling tidb-default config for tidb clusters", zap.Int("tidbClusterCount", totalCount))
	return nil
}

func backfillSingleTiDBClusterDefaultConfig(ctx context.Context, tidbCluster *entity.TiDBCluster, tidbGroupRepo repo.TiDBGroupRepo, clusterConfig clusterconfig.Client) error {
	tidbClusterId := tidbCluster.GetId()
	sharedPoolId := tidbCluster.GetSharedPoolId()
	client := clusterConfig.GetClient()

	// Check if tidb-default config already exists
	_, err := client.GetClusterConfigurationFile(ctx, &configcenter.GetClusterConfigurationFileRequest{
		ClusterType:           configcenter.ClusterType_LOGICAL_CLUSTER,
		ClusterId:             tidbClusterId,
		ConfigurationFilename: clusterconfig.TiDBDefaultConfigFileName,
	})
	if err == nil {
		tilog.Info(ctx, "tidb-default config already exists, skip backfill", zap.String("tidbClusterId", tidbClusterId))
		return nil
	}
	st, ok := status.FromError(err)
	if !ok || st.Code() != codes.NotFound {
		return fmt.Errorf("get tidb-default config: %w", err)
	}

	// tidb-default not found, try to derive from an existing tidb-{group} config first
	// List all tidb groups for this cluster
	tidbGroups, err := tidbGroupRepo.List(ctx, nil, nil, []string{tidbClusterId}, nil, false)
	if err != nil {
		return fmt.Errorf("list tidb groups: %w", err)
	}

	// Try to find an existing tidb-{group} config to use as template
	var templateConfig *configcenter.ConfigurationFile
	var templateGroupId string
	for _, tidbGroup := range tidbGroups {
		groupConfigName := clusterconfig.GetTiDBGroupConfigFileName(tidbGroup.GetId())
		resp, err := client.GetClusterConfigurationFile(ctx, &configcenter.GetClusterConfigurationFileRequest{
			ClusterType:           configcenter.ClusterType_LOGICAL_CLUSTER,
			ClusterId:             tidbClusterId,
			ConfigurationFilename: groupConfigName,
		})
		if err == nil {
			templateConfig = resp
			templateGroupId = tidbGroup.GetId()
			break
		}
	}

	// If no tidb-{group} config found, try to derive from sharedpool tidb template
	if templateConfig == nil {
		tilog.Info(ctx, "no existing tidb-{group} config found, try to derive from sharedpool tidb template",
			zap.String("tidbClusterId", tidbClusterId),
			zap.String("sharedPoolId", sharedPoolId))

		if sharedPoolId == "" {
			return fmt.Errorf("sharedPoolId is empty for tidbCluster %s, cannot derive tidb-default", tidbClusterId)
		}

		sharedPoolTidbConfig, err := client.GetClusterConfigurationFile(ctx, &configcenter.GetClusterConfigurationFileRequest{
			ClusterType:           configcenter.ClusterType_SHARED_POOL,
			ClusterId:             sharedPoolId,
			ConfigurationFilename: strings.ToLower(apiv1.ComponentType_TIDB.String()),
		})
		if err != nil {
			return fmt.Errorf("get sharedpool tidb template for sharedPool %s: %w", sharedPoolId, err)
		}

		templateConfig = sharedPoolTidbConfig
		templateGroupId = "sharedpool:" + sharedPoolId
	}

	tilog.Info(ctx, "creating tidb-default config",
		zap.String("tidbClusterId", tidbClusterId),
		zap.String("templateSource", templateGroupId),
		zap.Int64("templateId", templateConfig.GetTemplateId()),
	)

	// Create tidb-default by deriving from the template config
	_, err = client.CreateClusterConfigurationFile(ctx, &configcenter.CreateClusterConfigurationFileRequest{
		ClusterType: configcenter.ClusterType_LOGICAL_CLUSTER,
		ClusterId:   tidbClusterId,
		ConfigurationFile: &configcenter.ConfigurationFile{
			ClusterType:           configcenter.ClusterType_LOGICAL_CLUSTER,
			ConfigurationFilename: clusterconfig.TiDBDefaultConfigFileName,
			ConfigurationType:     strings.ToLower(apiv1.ComponentType_TIDB.String()),
			BelongToComponent:     strings.ToLower(apiv1.ComponentType_TIDB.String()),
			Format:                configcenter.Format_TOML,
			TemplateId:            templateConfig.GetTemplateId(),
			CustomParams:          templateConfig.GetCustomParams(),
		},
	})
	if err != nil {
		st, ok := status.FromError(err)
		if ok && st.Code() == codes.AlreadyExists {
			tilog.Info(ctx, "tidb-default config already exists (race condition)", zap.String("tidbClusterId", tidbClusterId))
			return nil
		}
		return fmt.Errorf("create tidb-default config: %w", err)
	}

	tilog.Info(ctx, "completed backfilling tidb-default config for tidb cluster", zap.String("tidbClusterId", tidbClusterId))
	return nil
}
