package applier

import (
	"context"
	"fmt"

	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	"github.com/tidbcloud/devops/pkg/infra/clients/configcentercli"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/infra/utils/pointer"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/api"
	"github.com/tidbcloud/devops/pkg/service/cloudconfig/cvt"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/types/known/structpb"

	ccproto "github.com/tidbcloud/config-center/proto/config_center"
)

const VersionFollowGlobal = "FOLLOW_GLOBAL"

type ConfigCenterTenantInitApplierRequest struct {
	ParamRequest              *ccproto.BatchUpdateTenantInitialCustomParamsRequest
	DefaultVersionRequest     *ccproto.UpdateDefaultVersionRequest
	DeleteVersionRequest      *ccproto.DeleteDefaultVersionRequest
	UpdateDefaultImageRequest []*ccproto.UpdateComponentImageRequest
}

type ConfigCenterTenantInitApplier struct {
	configcenter *configcentercli.Client
}

func NewConfigCenterTenantInitApplier(configcenter *configcentercli.Client) *ConfigCenterTenantInitApplier {
	return &ConfigCenterTenantInitApplier{
		configcenter: configcenter,
	}
}

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

func (a *ConfigCenterTenantInitApplier) AcceptVersionChange(verChange *ccent.ConfigurationVersionChange, changeType string) bool {
	return true
}

func (a *ConfigCenterTenantInitApplier) AcceptComponentChange(compChg *ccent.ConfigurationComponentChange) bool {
	return true
}

func (a *ConfigCenterTenantInitApplier) AcceptFileChange(fileChg *ccent.ConfigurationFileChange) bool {
	return true
}

func (a *ConfigCenterTenantInitApplier) Prepare(ctx context.Context, actx *ApplyContext) error {
	chg := actx.Input.Change
	changeType := api.ChangeType(chg.ChangeType)
	actx.ConfigCenterTenantInitApplierRequest = &ConfigCenterTenantInitApplierRequest{
		ParamRequest: &ccproto.BatchUpdateTenantInitialCustomParamsRequest{
			Requests: make([]*ccproto.UpdateTenantInitialCustomParamsRequest, 0),
		},
	}
	request := actx.ConfigCenterTenantInitApplierRequest

	// update tenant init configs
	for _, fileChg := range actx.Input.Files {
		req := &ccproto.UpdateTenantInitialCustomParamsRequest{
			ClusterType:       cvt.To.ConfigCenter.ClusterType(chg.TargetTenantInit.ClusterType),
			ConfigurationType: fileChg.ConfigType,
			TenantId:          chg.TargetTenantInit.TenantID,
			InitialParams:     make(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 tenant init config item failed: %w", err)
			}

			req.InitialParams[item.Name] = param
		}
		request.ParamRequest.Requests = append(request.ParamRequest.Requests, req)
	}

	// update tenant default version
	if actx.Input.Version != nil {
		if actx.Input.Version.To == VersionFollowGlobal {
			request.DeleteVersionRequest = &ccproto.DeleteDefaultVersionRequest{
				ClusterType: cvt.To.ConfigCenter.ClusterType(chg.TargetTenantInit.ClusterType),
				Component:   changeType.VersionComponent(),
				TenantId:    pointer.Pointer(chg.TargetTenantInit.TenantID),
			}
		} else {
			request.DefaultVersionRequest = &ccproto.UpdateDefaultVersionRequest{
				ClusterType: cvt.To.ConfigCenter.ClusterType(chg.TargetTenantInit.ClusterType),
				Component:   changeType.VersionComponent(),
				TenantId:    pointer.Pointer(chg.TargetTenantInit.TenantID),
				Version:     actx.Input.Version.To,
			}
		}
	}

	// update tenant init image tag
	// 1. get default version
	version, err := a.configcenter.ConfigAPI.GetDefaultVersion(ctx, &ccproto.GetDefaultVersionRequest{
		ClusterType: cvt.To.ConfigCenter.ClusterType(chg.TargetTenantInit.ClusterType),
		Component:   changeType.VersionComponent(),
		TenantId:    pointer.Pointer(chg.TargetTenantInit.TenantID),
	})
	if err != nil {
		return fmt.Errorf("get tenant init default version from config center failed: %w", err)
	}
	// if tenant default version is also updated, use the new version
	if actx.Input.Version != nil && actx.Input.Version.To != VersionFollowGlobal {
		version.Version = actx.Input.Version.To
	}
	// 2. get image base and tag by version

	request.UpdateDefaultImageRequest = make([]*ccproto.UpdateComponentImageRequest, 0)
	for _, compChg := range actx.Input.Components {
		if compChg.ModifiedOverwriteImageTag != nil {
			componentImage, err := a.configcenter.ConfigAPI.GetComponentImage(ctx, &ccproto.GetComponentImageRequest{
				ClusterType:      cvt.To.ConfigCenter.ClusterType(chg.TargetTenantInit.ClusterType),
				ComponentName:    compChg.Component,
				ComponentVersion: version.Version,
				TenantId:         pointer.Pointer(chg.TargetTenantInit.TenantID),
			})
			if err != nil {
				return fmt.Errorf("get tenant init component image from config center failed: %w", err)
			}
			request.UpdateDefaultImageRequest = append(request.UpdateDefaultImageRequest, &ccproto.UpdateComponentImageRequest{
				ClusterType:      cvt.To.ConfigCenter.ClusterType(chg.TargetTenantInit.ClusterType),
				ComponentName:    compChg.Component,
				ComponentVersion: version.Version,
				BaseImage:        componentImage.BaseImage,
				Tag:              compChg.ModifiedOverwriteImageTag.To,
				TenantId:         pointer.Pointer(chg.TargetTenantInit.TenantID),
			})
		}

	}

	return nil

}

func (a *ConfigCenterTenantInitApplier) Apply(ctx context.Context, actx *ApplyContext) error {
	logger := log.FromContext(ctx).WithName(a.Name())

	if actx.ApplyMode.Appliable(api.ConfigCenterApply) {

		request := actx.ConfigCenterTenantInitApplierRequest

		if len(request.ParamRequest.Requests) != 0 {
			logger.Info("applying tenant-init config change to config center", "req", protojson.Format(request.ParamRequest))
			_, err := a.configcenter.ConfigAPI.BatchUpdateTenantInitialCustomParams(ctx, request.ParamRequest)
			if err != nil {
				return fmt.Errorf("update tenant init config via config center failed: %w", err)
			}
		}

		if request.DeleteVersionRequest != nil {
			logger.Info("deleting tenant-init default version change to config center", "req", protojson.Format(request.DeleteVersionRequest))
			_, err := a.configcenter.ConfigAPI.DeleteDefaultVersion(ctx, request.DeleteVersionRequest)
			if err != nil {
				return fmt.Errorf("delete tenant init default version via config center failed: %w", err)
			}
		}

		for _, updateImageReq := range request.UpdateDefaultImageRequest {
			logger.Info("updating tenant-init default image change to config center", "req", protojson.Format(updateImageReq))
			_, err := a.configcenter.ConfigAPI.UpdateComponentImage(ctx, updateImageReq)
			if err != nil {
				return fmt.Errorf("update tenant init default image via config center failed: %w", err)
			}
		}

		if request.DefaultVersionRequest != nil {
			// TODO: change the api of cloud config not config center
			logger.Info("applying tenant-init default version change to config center", "req", protojson.Format(request.DefaultVersionRequest))
			_, err := a.configcenter.ConfigAPI.UpdateDefaultVersion(ctx, request.DefaultVersionRequest)
			if err != nil {
				return fmt.Errorf("update tenant init default version via config center failed: %w", err)
			}
		}

	} else {
		logger.Info("skip apply via config center cause apply method missed in apply mode")
	}

	return nil
}
func (a *ConfigCenterTenantInitApplier) CheckResult(ctx context.Context, rctx *CheckResultContext) (bool, string, error) {
	return true, "", nil
}

type OnlyConfigCenterClusterApplierRequest struct {
	UpdateMetaReq *ccproto.UpdateClusterMetaRequest
}

type OnlyConfigCenterClusterApplier struct {
	configcenter *configcentercli.Client
}

// accept componts and configs that only need to update config-center
func NewOnlyConfigCenterClusterApplier(configcenter *configcentercli.Client) *OnlyConfigCenterClusterApplier {
	return &OnlyConfigCenterClusterApplier{
		configcenter: configcenter,
	}
}

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

func (a *OnlyConfigCenterClusterApplier) AcceptVersionChange(verChange *ccent.ConfigurationVersionChange, changeType string) bool {
	return true
}

func (a *OnlyConfigCenterClusterApplier) AcceptComponentChange(compChg *ccent.ConfigurationComponentChange) bool {
	switch compChg.Component {
	case ccproto.DEDICATED_TIDB_COMPONENT_TIDB_LIGHTNING,
		ccproto.DEDICATED_TIDB_COMPONENT_DM,
		ccproto.DEDICATED_TIDB_COMPONENT_COMPACT_BACKUP_BR,
		ccproto.DEDICATED_TIDB_COMPONENT_COMPACT_BACKUP_TIKV:
		return true
	}
	return false
}

func (a *OnlyConfigCenterClusterApplier) AcceptFileChange(fileChg *ccent.ConfigurationFileChange) bool {
	return fileChg.Filename == ccproto.DEDICATED_TIDB_CONFIG_TIDB_LIGHTNING
}

func (a *OnlyConfigCenterClusterApplier) Prepare(ctx context.Context, actx *ApplyContext) error {
	chg := actx.Input.Change
	metaReq := &ccproto.UpdateClusterMetaRequest{
		ClusterType: cvt.To.ConfigCenter.ClusterType(chg.TargetCluster.ClusterType),
		ClusterId:   chg.TargetCluster.ClusterID,
	}

	for _, compChg := range actx.Input.Components {
		req := &ccproto.UpdateClusterComponentRequest{
			ComponentName: compChg.Component,
		}
		if actx.Input.Version != nil {
			req.Version = &actx.Input.Version.To
		}
		if compChg.ModifiedOverwriteImageTag != nil {
			req.OverwriteImageTag = &compChg.ModifiedOverwriteImageTag.To
		}
		// only for DM currnetly
		if chg.ApplyPolicy == ccent.ParameterApplyPolicyNewInstancesOnly {
			req.UpdateStrategy = pointer.Pointer(ccproto.UpdateStrategy_UPDATE_NEW)
		} else if chg.ApplyPolicy == ccent.ParameterApplyPolicyRestart {
			req.UpdateStrategy = pointer.Pointer(ccproto.UpdateStrategy_UPDATE_ALL)
		}
		if compChg.Component == ccproto.DEDICATED_TIDB_COMPONENT_DM {
			req.AllowMissing = true
		}
		metaReq.ComponentRequests = append(metaReq.ComponentRequests, req)
	}

	for _, fileChg := range actx.Input.Files {
		req := &ccproto.UpdateClusterConfigurationFileRequest{
			ConfigurationFilename: fileChg.Filename,
		}

		//  build the request for the config center
		if fileChg.ModifiedTemplate != nil {
			req.TemplateId = pointer.Pointer(int64(fileChg.ModifiedTemplate.To))
		}
		req.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)
			}

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

		metaReq.ConfigurationFileRequests = append(metaReq.ConfigurationFileRequests, req)
	}

	actx.OnlyConfigCenterClusterApplierRequest = &OnlyConfigCenterClusterApplierRequest{
		UpdateMetaReq: metaReq,
	}

	return nil
}

func (a *OnlyConfigCenterClusterApplier) Apply(ctx context.Context, actx *ApplyContext) error {
	logger := log.FromContext(ctx).WithName(a.Name())

	if actx.ApplyMode.Appliable(api.ConfigCenterApply) {
		request := actx.OnlyConfigCenterClusterApplierRequest
		if request != nil && request.UpdateMetaReq != nil {
			if request.UpdateMetaReq.ClusterType == ccproto.ClusterType_SHARED_POOL || request.UpdateMetaReq.ClusterType == ccproto.ClusterType_LOGICAL_CLUSTER {
				return fmt.Errorf("OnlyConfigCenterClusterApplier does not support nextgen cluster")
			}
			logger.Info("applying cluster config change to config center", "req", protojson.Format(request.UpdateMetaReq))
			_, err := a.configcenter.ConfigAPI.UpdateClusterMeta(ctx, request.UpdateMetaReq)
			if err != nil {
				return fmt.Errorf("update cluster config via config center failed: %w", err)
			}
		}

	} else {
		logger.Info("skip apply via config center cause apply method missed in apply mode")
	}

	return nil
}

func (a *OnlyConfigCenterClusterApplier) CheckResult(ctx context.Context, rctx *CheckResultContext) (bool, string, error) {
	return true, "", nil
}

type ConfigurationTemplateApplierRequest struct {
	Templates      *ccproto.BatchCreateConfigurationTemplatesRequest
	CustomTemplate *ccproto.CreateConfigurationTemplateRequest
}

type ConfigurationTemplateApplier struct {
	configcenter *configcentercli.Client
}

// only accept custom template currently
func NewConfigurationTemplateApplier(configcenter *configcentercli.Client) *ConfigurationTemplateApplier {
	return &ConfigurationTemplateApplier{
		configcenter: configcenter,
	}
}

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

func (a *ConfigurationTemplateApplier) AcceptVersionChange(verChange *ccent.ConfigurationVersionChange, changeType string) bool {
	return true
}

func (a *ConfigurationTemplateApplier) AcceptComponentChange(compChg *ccent.ConfigurationComponentChange) bool {
	return false
}

func (a *ConfigurationTemplateApplier) AcceptFileChange(fileChg *ccent.ConfigurationFileChange) bool {
	return true
}

func (a *ConfigurationTemplateApplier) Prepare(ctx context.Context, actx *ApplyContext) error {
	chg := actx.Input.Change
	if chg.TargetTemplate == nil {
		return fmt.Errorf("target template is nil")
	}

	// decide template type for every file.
	var fileTempType ccent.TemplateType
	switch chg.TargetTemplate.TemplateType {
	case ccent.TemplateTypeCustom:
		// for custom template, every file is a part of the custom template.
		fileTempType = ccent.TemplateTypeCustomPart
	case ccent.TemplateTypeTenantDefault:
		fileTempType = chg.TargetTemplate.TemplateType
	default:
		return fmt.Errorf("unsupported template type: %s", chg.TargetTemplate.TemplateType)
	}

	templates := make([]*ccproto.CreateConfigurationTemplateRequest, 0)
	clusterType := cvt.To.ConfigCenter.ClusterType(chg.TargetTemplate.ClusterType)
	for _, fileChg := range actx.Input.Files {
		req := &ccproto.CreateConfigurationTemplateRequest{
			ClusterType:       clusterType,
			ConfigurationType: fileChg.Filename,
			TemplateName:      chg.Reason,
			Data:              make(map[string]*ccproto.ParamItem),
			Tags: map[string]string{
				ccproto.TEMPLATE_TAG_TEMPLATE_TYPE: string(fileTempType),
			},
		}
		if chg.TargetTemplate.TemplateType == ccent.TemplateTypeCustom {
			req.TemplateName = fmt.Sprintf("%s_%s", chg.Reason, fileChg.Filename)
		}

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

			req.Data[item.Name] = param
		}

		templates = append(templates, req)
	}

	// add extra tags
	if chg.TargetTemplate.TemplateType == ccent.TemplateTypeTenantDefault {
		if chg.TargetTemplate.TenantID == nil {
			return fmt.Errorf("tenant default template must have tenant id")
		}

		for _, req := range templates {
			req.Tags[ccproto.TEMPLATE_TAG_OWNER_TENANT_ID] = *chg.TargetTemplate.TenantID
		}
	}

	actx.ConfigurationTemplateApplierRequest = &ConfigurationTemplateApplierRequest{
		Templates: &ccproto.BatchCreateConfigurationTemplatesRequest{
			Requests: templates,
		},
	}

	if chg.TargetTemplate.TemplateType == ccent.TemplateTypeCustom {
		// for custom template, need to create a custom template to group all parts.
		actx.ConfigurationTemplateApplierRequest.CustomTemplate = &ccproto.CreateConfigurationTemplateRequest{
			ClusterType:       clusterType,
			ConfigurationType: "custom_template",
			TemplateName:      chg.Reason,
			Data:              make(map[string]*ccproto.ParamItem),
			Tags: map[string]string{
				ccproto.TEMPLATE_TAG_TEMPLATE_TYPE:           string(ccent.TemplateTypeCustom),
				ccproto.TEMPLATE_TAG_EFFECTIVE_MINOR_VERSION: chg.TargetVersion,
				ccproto.TEMPLATE_TAG_CREATOR_EMAIL:           chg.Applicant,
			},
		}
	}

	return nil
}

func (a *ConfigurationTemplateApplier) Apply(ctx context.Context, actx *ApplyContext) error {
	logger := log.FromContext(ctx).WithName(a.Name())

	if actx.ApplyMode.Appliable(api.ConfigCenterApply) {
		request := actx.ConfigurationTemplateApplierRequest
		if request != nil && len(request.Templates.Requests) > 0 {
			logger.Info("applying cluster config change to config center", "req", protojson.Format(request.Templates))
			resp, err := a.configcenter.ConfigAPI.BatchCreateConfigurationTemplates(ctx, request.Templates)
			if err != nil {
				return fmt.Errorf("update cluster config via config center failed: %w", err)
			}

			if request.CustomTemplate != nil {
				for _, template := range resp.Templates {
					request.CustomTemplate.Data[template.ConfigurationType] = &ccproto.ParamItem{
						Value:   structpb.NewNumberValue(float64(template.TemplateId)),
						Comment: fmt.Sprintf("template id for %s", template.ConfigurationType),
					}
				}
				logger.Info("applying custom template to config center", "req", protojson.Format(request.CustomTemplate))
				_, err = a.configcenter.ConfigAPI.CreateConfigurationTemplate(ctx, request.CustomTemplate)
				if err != nil {
					return fmt.Errorf("update cluster config via config center failed: %w", err)
				}
			}
		}

	} else {
		logger.Info("skip apply via config center cause apply method missed in apply mode")
	}

	return nil
}

func (a *ConfigurationTemplateApplier) CheckResult(ctx context.Context, rctx *CheckResultContext) (bool, string, error) {
	return true, "", nil
}
