package action

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"github.com/samber/lo"
	"github.com/tidbcloud/fastflow/pkg/entity/run"
	regionaldedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/clusterconfig"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/regionserver"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	clusterrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/cluster"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/region"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/service/workflow/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

const (
	ApplyCluster = "apply_cluster"
)

type ApplyClusterAction struct {
	regionalCli      regionserver.Client
	clusterRepo      clusterrepo.Repo
	regionRepo       region.Repo
	clusterConfigCli clusterconfig.Client
}

func NewApplyClusterAction(
	regionalCli regionserver.Client,
	clusterRepo clusterrepo.Repo,
	regionRepo region.Repo,
	clusterConfigCli clusterconfig.Client,
) *ApplyClusterAction {
	return &ApplyClusterAction{
		regionalCli:      regionalCli,
		clusterRepo:      clusterRepo,
		regionRepo:       regionRepo,
		clusterConfigCli: clusterConfigCli,
	}
}

func (a *ApplyClusterAction) Name() string {
	return ApplyCluster
}

func (a *ApplyClusterAction) ParameterNew() any {
	return &common.ClusterIdParameter{}
}

func (a *ApplyClusterAction) Run(ctx run.ExecuteContext, params any) error {
	cctx, span := workflow.GenerateContext(ctx)

	clusterId, err := common.GetClusterId(params)
	if err != nil {
		tilog.Error(cctx, "failed to get cluster id", zap.Error(err))
		return err
	}

	tilog.Info(cctx, "start to apply cluster", zap.Uint64("cluster_id", clusterId))
	defer func() {
		tilog.Info(cctx, "end to apply cluster", zap.Uint64("cluster_id", clusterId))
		span.End()
	}()

	cluster, err := a.clusterRepo.Get(cctx, clusterId, false)
	if err != nil {
		tilog.Error(cctx, "failed to get cluster", zap.Error(err))
		return err
	}

	regionEntity, err := a.regionRepo.SelectRegionById(cctx, cluster.RegionId, cluster.GetOrganizationId())
	if err != nil {
		tilog.Error(cctx, "failed to find region", zap.Error(err))
		return err
	}

	// Try to get pre-built SetVersionAndConfigurationsRequest from context or share data
	setReq := &regionaldedicatedv1.SetVersionAndConfigurationsRequest{}
	setReqJson, ok := getVarFromContextOrShareData(ctx, common.KeySetVersionAndConfigurationsRequest)
	if !ok {
		tilog.Error(cctx, "not found the key", zap.String("key", common.KeySetVersionAndConfigurationsRequest))
	}
	err = protojson.Unmarshal([]byte(setReqJson), setReq)
	if err != nil {
		tilog.Error(cctx, "unmarshal set version & configurations request failed", zap.String("data", setReqJson), zap.Error(err))
	}

	// Compatible for existing unfinished workflow tasks.
	// If the SetVersionAndConfigurationsRequest is not found in the context, rebuild it.
	if !ok || err != nil || setReq.GetClusterId() == "" {
		tilog.Warn(cctx, "the SetVersionAndConfigurationsRequest is not found or invalid, rebuild it")

		maskJson, ok := getVarFromContextOrShareData(ctx, common.KeyClusterApplyMask)
		if !ok {
			tilog.Error(cctx, "not found the key", zap.String("key", common.KeyClusterApplyMask))
			return fmt.Errorf("not found the %s key", common.KeyClusterApplyMask)
		}

		var clusterApplyMask globaldedicatedv1.ClusterApplyMask
		if err := protojson.Unmarshal([]byte(maskJson), &clusterApplyMask); err != nil {
			tilog.Error(cctx, "unmarshal cluster apply mask failed", zap.Error(err))
			return err
		}
		setReq, _, err = BuildSetVersionAndConfigurationsRequest(cctx, a.clusterConfigCli, cluster, &clusterApplyMask)
		if err != nil {
			tilog.Error(cctx, "build apply cluster request failed", zap.Error(err))
			return err
		}
	}

	if _, err := a.regionalCli.SetVersionAndConfigurations(cctx, regionEntity, setReq); err != nil {
		tilog.Error(cctx, "failed to apply cluster", zap.Error(err))
		return err
	}

	return nil
}

func BuildSetVersionAndConfigurationsRequest(
	ctx context.Context,
	clusterConfigCli clusterconfig.Client,
	cluster *entity.Cluster,
	applyMask *globaldedicatedv1.ClusterApplyMask,
) (*regionaldedicatedv1.SetVersionAndConfigurationsRequest, map[string][]string, bizerr.BizError) {
	// Default values.
	req := &regionaldedicatedv1.SetVersionAndConfigurationsRequest{
		ClusterId:      strconv.FormatUint(cluster.ClusterId, 10),
		Strategy:       mapStrategy(applyMask),
		IdempotencyKey: workflow.GetTraceId(ctx),
	}

	operations := make(map[string][]string)

	addOp := func(scope, op string) {
		operations[scope] = append(operations[scope], req.GetStrategy().String()+"_"+op)
	}

	var err bizerr.BizError
	switch {
	case applyMask.GetVersion():
		// get cluster component configurations from config center
		req.ComponentConfigurations, err = clusterConfigCli.ListComponentConfigurations(ctx, cluster)
		if err != nil {
			return nil, nil, err
		}

		// get image tag from config center
		req.ClusterVersion, err = clusterConfigCli.GetClusterComponentsVersion(ctx, cluster)
		if err != nil {
			return nil, nil, err
		}

		// Filter out TIPROXY (unchanged logic)
		req.ComponentConfigurations = lo.Filter(
			req.ComponentConfigurations,
			func(cfg *regionaldedicatedv1.ComponentConfiguration, _ int) bool {
				return cfg.ComponentType != regionaldedicatedv1.ComponentType_TIPROXY
			},
		)
		req.ClusterVersion.ComponentVersions = lo.Filter(
			req.ClusterVersion.ComponentVersions,
			func(cv *regionaldedicatedv1.ClusterVersion_ComponentVersion, _ int) bool {
				return cv.ComponentType != regionaldedicatedv1.ComponentType_TIPROXY
			},
		)

		addOp("cluster", "upgrade_version")

	case len(applyMask.GetComponentApplyMasks()) > 0:
		for _, componentMask := range applyMask.GetComponentApplyMasks() {
			ct := componentMask.ComponentType
			key := strings.ToLower(ct.String())

			if componentMask.GetConfiguration() {
				componentCfg, cfgErr := clusterConfigCli.GetComponentConfiguration(ctx, cluster, ct)
				if cfgErr != nil {
					return nil, nil, cfgErr
				}
				req.ComponentConfigurations = append(req.ComponentConfigurations, componentCfg)
				addOp(key, "change_config")
			}

			if componentMask.GetImage() {
				componentVersion, vErr := clusterConfigCli.GetClusterComponentVersion(ctx, cluster, ct)
				if vErr != nil {
					return nil, nil, vErr
				}
				if req.ClusterVersion == nil {
					req.ClusterVersion = &regionaldedicatedv1.ClusterVersion{
						ComponentVersions: nil,
						ClusterVersion:    cluster.Version,
					}
				}
				req.ClusterVersion.ComponentVersions = append(req.ClusterVersion.ComponentVersions, componentVersion)
				addOp(key, "upgrade_version")
			}
		}
	}

	return req, operations, nil
}

// Helper: map strategy (honors deprecated hot_reload flag first).
func mapStrategy(mask *globaldedicatedv1.ClusterApplyMask) regionaldedicatedv1.SetVersionAndConfigurationsRequest_Strategy {
	if mask == nil {
		return regionaldedicatedv1.SetVersionAndConfigurationsRequest_ROLLING_RESTART
	}
	// Deprecated flag still has priority for backward compatibility
	if mask.GetHotReload() {
		return regionaldedicatedv1.SetVersionAndConfigurationsRequest_HOT_RELOAD
	}
	switch mask.GetStrategy() {
	case globaldedicatedv1.ClusterApplyMask_HOT_RELOAD:
		return regionaldedicatedv1.SetVersionAndConfigurationsRequest_HOT_RELOAD
	case globaldedicatedv1.ClusterApplyMask_CHANGE_CM_ONLY:
		return regionaldedicatedv1.SetVersionAndConfigurationsRequest_CHANGE_CM_ONLY
	case globaldedicatedv1.ClusterApplyMask_ENSURE_UPDATE_TO_TC:
		return regionaldedicatedv1.SetVersionAndConfigurationsRequest_ENSURE_UPDATE_TO_TC
	default:
		return regionaldedicatedv1.SetVersionAndConfigurationsRequest_ROLLING_RESTART
	}
}

// Helper: get variable from context or share data (for shutdown upgrade workflow compatibility).
func getVarFromContextOrShareData(ctx run.ExecuteContext, key string) (string, bool) {
	// Try context variables first
	if value, ok := ctx.GetVar(key); ok {
		return value, true
	}
	// Fall back to share data
	if value, ok := ctx.ShareData().Get(key); ok && value != "" {
		return value, true
	}
	return "", false
}
