package action

import (
	"errors"
	"fmt"

	"github.com/pingcap/kvproto/pkg/keyspacepb"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/domain/workflow"
	"github.com/tidbcloud/cluster-service-ng/pkg/clustergateway"
	"github.com/tidbcloud/cluster-service-ng/pkg/runtime/actions"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"github.com/tidbcloud/cluster-service-ng/pkg/utils"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"
)

type SetKeySpaceAction struct {
	sharedPoolRepo    repo.SharedPoolRepo
	tidbClusterRepo   repo.TiDBClusterRepo
	clusterGatewayCli clustergateway.Client
}

const SetKeySpaceActionName = "set-keyspace"

func NewSetKeySpaceAction(
	sharedPoolRepo repo.SharedPoolRepo,
	tidbClusterRepo repo.TiDBClusterRepo,
	clusterGatewayCli clustergateway.Client,
) actions.Action {
	return &SetKeySpaceAction{
		sharedPoolRepo:    sharedPoolRepo,
		tidbClusterRepo:   tidbClusterRepo,
		clusterGatewayCli: clusterGatewayCli,
	}
}

func (a *SetKeySpaceAction) Name() string {
	return SetKeySpaceActionName
}

func (a *SetKeySpaceAction) Invoke(args actions.ActionArgs) error {
	// generate context
	ctx := utils.GenerateContext(args)
	tilog.Info(ctx, "set keyspace action invoked")

	// get sharedPool id
	sharedPoolId, err := args.GetString(workflow.SharedPoolIdKey)
	if err != nil {
		tilog.Error(ctx, "failed to get sharedPool id", zap.Error(err))
		return fmt.Errorf("failed to get sharedPool id: %w", err)
	}

	cmekId, err := args.GetString(workflow.CmekIdKey)
	if err != nil {
		tilog.Warn(ctx, "failed to get cmek id from action args", zap.Error(err))
	}

	// load sharedPool
	sharedPool, err := a.sharedPoolRepo.Load(ctx, sharedPoolId, false)
	if err != nil {
		tilog.Error(ctx, "failed to load sharedPool", zap.String("sharedPoolId", sharedPoolId), zap.Error(err))
		return fmt.Errorf("failed to load sharedPool: %w", err)
	}

	tidbClusterId, err := args.GetString(workflow.TiDBClusterIdKey)
	if err != nil {
		tilog.Error(ctx, "failed to get tidb cluster id", zap.Error(err))
		return fmt.Errorf("failed to get tidb cluster id: %w", err)
	}

	// load tidb cluster
	tidbCluster, err := a.tidbClusterRepo.Load(ctx, tidbClusterId, false)
	if err != nil {
		tilog.Error(ctx, "failed to load tidb cluster", zap.String("tidbClusterId", tidbClusterId), zap.Error(err))
		return fmt.Errorf("failed to load tidb cluster: %w", err)
	}

	// first get keyspace
	ksResp, err := a.clusterGatewayCli.GetKeySpace(ctx, sharedPool, tidbCluster.GetId())
	switch {
	case err == nil:
		tilog.Info(ctx, "keyspace already exists", zap.String("keyspace", tidbCluster.GetId()), zap.String("sharedPoolId", sharedPoolId))
		if ksResp.GetState() != keyspacepb.KeyspaceState_ENABLED {
			// not enabled, wait to enable it
			return fmt.Errorf("keyspace %s is not enabled yet, current state: %s", tidbCluster.GetId(), ksResp.GetState().String())
		}
	case clustergateway.IsNotFoundError(err):
		securityConfig, err := getTiDBClusterSecurityConfigurationFromArgs(args)
		if err != nil {
			tilog.Error(ctx, "failed to get tidb cluster security configuration from action args", zap.String("tidbClusterId", tidbClusterId), zap.Error(err))
			return fmt.Errorf("failed to get tidb cluster security configuration from action args: %w", err)
		}
		// keyspace not found, create it
		err = a.clusterGatewayCli.CreateKeySpace(ctx, sharedPool, tidbCluster.GetId(), cmekId, securityConfig.GetEncryptionContext())
		if err != nil {
			tilog.Error(ctx, "failed to create keyspace", zap.String("keyspace", tidbCluster.GetId()), zap.Error(err))
			return fmt.Errorf("failed to create keyspace: %w", err)
		}
		tilog.Info(ctx, "create keyspace successfully", zap.String("keyspace", tidbCluster.GetId()), zap.String("sharedPoolId", sharedPoolId))
	default:
		tilog.Error(ctx, "failed to get keyspace", zap.String("keyspace", tidbCluster.GetId()), zap.Error(err))
		return fmt.Errorf("failed to get keyspace: %w", err)
	}

	// set resource group for keyspace
	err = a.clusterGatewayCli.SetKeySpaceResourceGroup(ctx, sharedPool, tidbCluster.GetId(), &clustergateway.RCURequest{
		ServiceLimit: float64(tidbCluster.RequestUnit.Max),
	})
	if err != nil {
		tilog.Error(ctx, "failed to create resource group", zap.String("resource group", tidbCluster.GetId()), zap.Error(err))
		return fmt.Errorf("failed to create resource group: %w", err)
	}

	// Set RU version via the controller config API.
	if ruVersion := tidbCluster.GetRuVersion(); ruVersion > 0 {
		if err := a.clusterGatewayCli.SetKeySpaceRuVersion(ctx, sharedPool, tidbCluster.GetId(), &clustergateway.SetKeyspaceRuVersionRequest{
			RuVersion: ruVersion,
		}); err != nil {
			if errors.Is(err, clustergateway.ErrRuVersionNotSupported) {
				if ruVersion == 2 {
					tilog.Warn(ctx, "PD does not support ru_version API, RU version 2 not applied",
						zap.String("keyspace", tidbCluster.GetId()),
						zap.String("shared_pool", sharedPool.GetId()),
					)
					RuVersionUnsupportedTotal.WithLabelValues(sharedPool.GetId(), tidbCluster.GetId()).Inc()
				}
			} else {
				tilog.Error(ctx, "failed to set ru version", zap.String("keyspace", tidbCluster.GetId()), zap.Error(err))
				return fmt.Errorf("failed to set ru version: %w", err)
			}
		}
	}

	tilog.Info(ctx, "create resource group successfully",
		zap.String("resource group", tidbCluster.GetId()),
		zap.String("sharedPoolId", sharedPoolId),
		zap.Uint32("service limit", tidbCluster.RequestUnit.Max),
		zap.Int32("ru version", tidbCluster.GetRuVersion()),
	)

	return nil
}

func getTiDBClusterSecurityConfigurationFromArgs(args actions.ActionArgs) (*entity.ClusterSecurityConfiguration, error) {
	tidbClusterStr, err := args.GetString(workflow.TiDBClusterKey)
	if err != nil {
		return nil, err
	}

	var tidbCluster entity.TiDBCluster
	if err := protojson.Unmarshal([]byte(tidbClusterStr), &tidbCluster); err != nil {
		return nil, fmt.Errorf("failed to unmarshal tidb cluster: %w", err)
	}

	return tidbCluster.GetSecurityConfiguration(), nil
}
