package applier

import (
	"context"
	"fmt"
	"strconv"

	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	"github.com/tidbcloud/devops/pkg/infra/clients/configcentercli"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/container"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/api"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/cvt"

	ccproto "github.com/tidbcloud/config-center/proto/config_center"
	dedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	openapiv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"google.golang.org/protobuf/encoding/protojson"
)

type MgmtServiceApplierRequest struct {
	ClusterType ccproto.ClusterType
	ClusterId   string

	CCRequest   *ccproto.UpdateClusterMetaRequest
	MgmtRequest *mgmtv1.ApplyClusterRequest
}

func (r *MgmtServiceApplierRequest) ModifyConfigurationFile(file string, configType string,
	set func(ccreq *ccproto.UpdateClusterConfigurationFileRequest, mgmtReq *dedicatedv1.ClusterApplyMask_ComponentApplyMask) error,
) error {

	var ccreq *ccproto.UpdateClusterConfigurationFileRequest
	var mgmtreq *dedicatedv1.ClusterApplyMask_ComponentApplyMask

	for _, tmp := range r.CCRequest.ConfigurationFileRequests {
		if tmp.ConfigurationFilename == file {
			ccreq = tmp
			break
		}
	}
	for _, tmp := range r.MgmtRequest.ClusterApplyMask.ComponentApplyMasks {
		if tmp.ComponentType == cvt.To.Mgmt.ConfigType(configType) {
			mgmtreq = tmp
			break
		}
	}

	if ccreq == nil {
		ccreq = &ccproto.UpdateClusterConfigurationFileRequest{
			ClusterType:           r.ClusterType,
			ClusterId:             r.ClusterId,
			ConfigurationFilename: file,
			ConfigurationType:     &configType,
		}
		r.CCRequest.ConfigurationFileRequests = append(r.CCRequest.ConfigurationFileRequests, ccreq)
	}
	if mgmtreq == nil {
		mgmtreq = &dedicatedv1.ClusterApplyMask_ComponentApplyMask{
			ComponentType: cvt.To.Mgmt.ConfigType(configType),
		}
		r.MgmtRequest.ClusterApplyMask.ComponentApplyMasks = append(r.MgmtRequest.ClusterApplyMask.ComponentApplyMasks, mgmtreq)
	}

	return set(ccreq, mgmtreq)
}

func (r *MgmtServiceApplierRequest) ModifyComponent(comp string,
	set func(ccReq *ccproto.UpdateClusterComponentRequest, mgmtReq *dedicatedv1.ClusterApplyMask_ComponentApplyMask) error,
) error {

	var ccreq *ccproto.UpdateClusterComponentRequest
	var mgmtreq *dedicatedv1.ClusterApplyMask_ComponentApplyMask

	for _, tmpReq := range r.CCRequest.ComponentRequests {
		if tmpReq.ComponentName == comp {
			ccreq = tmpReq
			break
		}
	}
	for _, tmp := range r.MgmtRequest.ClusterApplyMask.ComponentApplyMasks {
		if tmp.ComponentType == cvt.To.Mgmt.Component(comp) {
			mgmtreq = tmp
			break
		}
	}

	if ccreq == nil {
		ccreq = &ccproto.UpdateClusterComponentRequest{
			ClusterType:   r.ClusterType,
			ClusterId:     r.ClusterId,
			ComponentName: comp,
		}
		r.CCRequest.ComponentRequests = append(r.CCRequest.ComponentRequests, ccreq)
	}
	if mgmtreq == nil {
		mgmtreq = &dedicatedv1.ClusterApplyMask_ComponentApplyMask{
			ComponentType: cvt.To.Mgmt.Component(comp),
		}
		r.MgmtRequest.ClusterApplyMask.ComponentApplyMasks = append(r.MgmtRequest.ClusterApplyMask.ComponentApplyMasks, mgmtreq)
	}

	return set(ccreq, mgmtreq)
}

type MgmtServiceApplier struct {
	coresvc      *coresvccli.CoreSvcCli
	configcenter *configcentercli.Client
}

func NewMgmtServiceApplier(coresvc *coresvccli.CoreSvcCli, configcenter *configcentercli.Client) *MgmtServiceApplier {
	return &MgmtServiceApplier{
		coresvc:      coresvc,
		configcenter: configcenter,
	}
}

func (a *MgmtServiceApplier) Name() string {
	return "MgmtServiceApplier"
}

func (a *MgmtServiceApplier) AcceptVersionChange(verChange *ccent.ConfigurationVersionChange, changeType string) bool {
	return changeType != string(api.ChangeTypeTiCDC)
}

func (a *MgmtServiceApplier) AcceptComponentChange(compChg *ccent.ConfigurationComponentChange) bool {
	return a.acceptComponent(compChg.Component)
}

func (a *MgmtServiceApplier) AcceptFileChange(fileChg *ccent.ConfigurationFileChange) bool {
	return a.acceptFile(fileChg.ConfigType)
}

func (a *MgmtServiceApplier) acceptComponent(comp string) bool {
	return cvt.To.Mgmt.Component(comp) != dedicatedv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED
}

func (a *MgmtServiceApplier) acceptFile(configType string) bool {
	return cvt.To.Mgmt.ConfigType(configType) != dedicatedv1.ComponentType_COMPONENT_TYPE_UNSPECIFIED
}

func (a *MgmtServiceApplier) Prepare(ctx context.Context, actx *ApplyContext) error {
	chg := actx.Input.Change

	clusterIDInt, err := strconv.ParseUint(chg.TargetCluster.ClusterID, 10, 64)
	if err != nil {
		return fmt.Errorf("parse cluster id to int failed: %w", err)
	}

	actx.MgmtServiceApplierRequest = &MgmtServiceApplierRequest{
		ClusterType: cvt.To.ConfigCenter.ClusterType(chg.TargetCluster.ClusterType),
		ClusterId:   chg.TargetCluster.ClusterID,

		MgmtRequest: &mgmtv1.ApplyClusterRequest{
			ClusterId: clusterIDInt,
			ClusterApplyMask: &dedicatedv1.ClusterApplyMask{
				Version:             false, // set later
				ComponentApplyMasks: nil,   // set later
				Strategy:            cvt.To.Mgmt.ApplyPolicy(chg.ApplyPolicy),
			},
			StatusId: 0, // set in Apply
		},
		CCRequest: &ccproto.UpdateClusterMetaRequest{
			ClusterType:               cvt.To.ConfigCenter.ClusterType(chg.TargetCluster.ClusterType),
			ClusterId:                 chg.TargetCluster.ClusterID,
			Version:                   nil, // set later
			ComponentRequests:         nil, // set later
			ConfigurationFileRequests: nil, // set later
		},
	}
	request := actx.MgmtServiceApplierRequest

	// update configuration files from file changes.
	for _, fileChg := range actx.Input.Files {
		filename := fileChg.Filename
		configType := fileChg.ConfigType

		err := request.ModifyConfigurationFile(filename, configType,
			func(ccreq *ccproto.UpdateClusterConfigurationFileRequest, mgmtReq *dedicatedv1.ClusterApplyMask_ComponentApplyMask) error {
				// build the request for the mgmt service
				mgmtReq.Configuration = true

				//  build the request for the config center
				if fileChg.ModifiedTemplate != nil {
					ccreq.TemplateId = pointer.Pointer(int64(fileChg.ModifiedTemplate.To))
				}
				ccreq.CustomParams = map[string]*ccproto.ParamItem{}
				for _, item := range fileChg.AllParameters {
					if item.Source != ccent.ParameterCustom { // skip non-custom parameters
						continue
					}

					param, err := cvt.To.ConfigCenter.NamedParameter(item.NamedParameter)
					if err != nil {
						return fmt.Errorf("convert cluster config item failed: %w", err)
					}

					ccreq.CustomParams[item.Name] = param
				}
				ccreq.PreviousText = pointer.Pointer(fileChg.PreviousText)
				ccreq.AllowMissing = true
				ccreq.ClearCustomParams = fileChg.ClearCustomParams

				return nil
			},
		)
		if err != nil {
			return err
		}
	}

	// update components
	if actx.Input.Version != nil && actx.Input.Change.ChangeType == string(api.ChangeTypeTidb) {
		request.MgmtRequest.ClusterApplyMask.Version = true
	}
	for _, compChg := range actx.Input.Components {
		err := request.ModifyComponent(compChg.Component,
			func(ccReq *ccproto.UpdateClusterComponentRequest, mgmtReq *dedicatedv1.ClusterApplyMask_ComponentApplyMask) error {
				mgmtReq.Image = true

				if actx.Input.Version != nil {
					ccReq.Version = &actx.Input.Version.To
				}
				if compChg.ModifiedOverwriteImageTag != nil {
					ccReq.OverwriteImageTag = &compChg.ModifiedOverwriteImageTag.To
				}

				return nil
			},
		)
		if err != nil {
			return err
		}
	}

	return nil
}

func (a *MgmtServiceApplier) Apply(ctx context.Context, actx *ApplyContext) error {
	chg := actx.Input.Change
	request := actx.MgmtServiceApplierRequest
	logger := log.FromContext(ctx).WithName(a.Name())

	clusterIDInt, err := strconv.ParseUint(chg.TargetCluster.ClusterID, 10, 64)
	if err != nil {
		return fmt.Errorf("parse cluster id to int failed: %w", err)
	}

	randomID := container.RandomString(6)
	statusName := fmt.Sprintf("apply-cluster-%d-%s", clusterIDInt, randomID)

	// 1. set status to applying
	setStatusReq := &mgmtv1.SetClusterCustomStatusRequest{
		CustomStatus: &mgmtv1.ClusterCustomStatus{
			ClusterId:      clusterIDInt,
			StatusType:     mgmtv1.CustomStatusType_CUSTOM_STATUS_TYPE_APPLY,
			StatusName:     statusName,
			OverrideStatus: openapiv1beta1.ClusterState_MODIFYING.Enum(),
			Mutex: &mgmtv1.ClusterCustomStatus_CustomMutex{
				StatusType: []mgmtv1.CustomStatusType{
					mgmtv1.CustomStatusType_CUSTOM_STATUS_TYPE_APPLY,
				},
				ClusterAction: []mgmtv1.ClusterAction{
					mgmtv1.ClusterAction_CLUSTER_ACTION_SCALE_CLUSTER,
					mgmtv1.ClusterAction_CLUSTER_ACTION_PAUSE_CLUSTER,
					mgmtv1.ClusterAction_CLUSTER_ACTION_MODIFY_CLUSTER,
				},
			},
		},
	}
	logger.Info("setting cluster custom status before applying", "req", protojson.Format(setStatusReq))
	setStatusResp, err := a.coresvc.Mgmt.ClusterCustomStatus.SetClusterCustomStatus(ctx, setStatusReq)
	if err != nil {
		return fmt.Errorf("set cluster custom status via mgmt failed: %w", err)
	}

	// last. revert status
	defer func() {
		revertStatusReq := &mgmtv1.DeleteClusterCustomStatusRequest{
			ClusterId: clusterIDInt,
			StatusId:  setStatusResp.StatusId,
		}
		logger.Info("revert cluster cluster custom status", "req", protojson.Format(revertStatusReq))
		_, err = a.coresvc.Mgmt.ClusterCustomStatus.DeleteClusterCustomStatus(ctx, revertStatusReq)
		if err != nil {
			logger.Error(err, "revert cluster custom status failed")
		}
	}()

	// 2. apply the file change via the config center
	if actx.ApplyMode.Appliable(api.ConfigCenterApply) {
		logger.Info("applying cluster config change via config center", "req", protojson.Format(request.CCRequest))
		_, err = a.configcenter.ConfigAPI.UpdateClusterMeta(ctx, request.CCRequest)
		if err != nil {
			return fmt.Errorf("apply cluster config via config center failed: %w", err)
		}
	} else {
		logger.Info("skip apply via config center cause apply method missed in apply mode")
	}

	// 3. apply the change via the mgmt service
	if actx.ApplyMode.Appliable(api.MgmtServiceApply) {
		request.MgmtRequest.StatusId = setStatusResp.StatusId
		logger.Info("applying cluster config change via mgmt service", "req", protojson.Format(request.MgmtRequest))
		_, err = a.coresvc.Mgmt.Cluster.ApplyCluster(ctx, request.MgmtRequest)
		if err != nil {
			return fmt.Errorf("apply cluster config via mgmt failed: %w", err)
		}
	} else {
		logger.Info("skip apply via mgmt service cause apply method missed in apply mode")
	}

	return nil
}

func (a *MgmtServiceApplier) CheckResult(ctx context.Context, rctx *CheckResultContext) (bool, string, error) {
	chg := rctx.Input.Change

	clusterIDInt, err := strconv.ParseUint(chg.TargetCluster.ClusterID, 10, 64)
	if err != nil {
		return false, "", fmt.Errorf("parse cluster id to int failed: %w", err)
	}

	cluster, err := a.coresvc.Mgmt.GetCluster(ctx, clusterIDInt)
	if err != nil {
		return false, "", fmt.Errorf("query cluster failed: %w", err)
	}

	state := cluster.GetDedicatedCluster().State
	if state == dedicatedv1.Cluster_AVAILABLE || state == dedicatedv1.Cluster_DELETED {
		return true, "", nil
	}

	return false, fmt.Sprintf("cluster state still is %s", state), nil
}
