package applier

import (
	"context"
	"encoding/json"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	clusterng "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	ccent "github.com/tidbcloud/devops/entity/cloudconfig"
	eventent "github.com/tidbcloud/devops/entity/event"
	"github.com/tidbcloud/devops/pkg/infra/clients/configcentercli"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/clients/mgmthttpcli"
	"github.com/tidbcloud/devops/pkg/infra/eventbus"
	"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"
	evbus "github.com/tidbcloud/eventbus-datastruct/event"

	ccproto "github.com/tidbcloud/config-center/proto/config_center"
	nextgenv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"google.golang.org/protobuf/encoding/protojson"
)

type NextGenApplierRequest struct {
	ClusterType ccproto.ClusterType
	ClusterId   string

	SharedPoolID  string
	LogicalRegion string
	CloudProvider string

	CCRequest     *ccproto.UpdateClusterMetaRequest
	ClusterSvcReq *clusterng.SetSharedPoolComponentConfigVersionRequest
}

func (r *NextGenApplierRequest) ModifyConfigurationFile(file string, configType string,
	set func(ccreq *ccproto.UpdateClusterConfigurationFileRequest) error,
) error {

	var ccreq *ccproto.UpdateClusterConfigurationFileRequest

	for _, tmp := range r.CCRequest.ConfigurationFileRequests {
		if tmp.ConfigurationFilename == file {
			ccreq = tmp
			break
		}
	}

	if ccreq == nil {
		ccreq = &ccproto.UpdateClusterConfigurationFileRequest{
			ConfigurationFilename: file,
			ConfigurationType:     &configType,
		}
		r.CCRequest.ConfigurationFileRequests = append(r.CCRequest.ConfigurationFileRequests, ccreq)
	}

	return set(ccreq)
}

func (r *NextGenApplierRequest) ModifyComponent(comp string,
	set func(ccReq *ccproto.UpdateClusterComponentRequest) error,
) error {

	var ccreq *ccproto.UpdateClusterComponentRequest

	for _, tmpReq := range r.CCRequest.ComponentRequests {
		if tmpReq.ComponentName == comp {
			ccreq = tmpReq
			break
		}
	}

	if ccreq == nil {
		ccreq = &ccproto.UpdateClusterComponentRequest{
			ComponentName: comp,
		}
		r.CCRequest.ComponentRequests = append(r.CCRequest.ComponentRequests, ccreq)
	}

	return set(ccreq)
}

type NextgenApplier struct {
	coresvc      *coresvccli.CoreSvcCli
	configcenter *configcentercli.Client
	mgmthttpcli  mgmthttpcli.MgmtHTTPClient
	eventBus     *eventbus.EventBus
}

func NewNextGenApplier(coresvc *coresvccli.CoreSvcCli, mgmthttpcli mgmthttpcli.MgmtHTTPClient, configcenter *configcentercli.Client, eventbus *eventbus.EventBus) *NextgenApplier {
	return &NextgenApplier{
		coresvc:      coresvc,
		mgmthttpcli:  mgmthttpcli,
		configcenter: configcenter,
		eventBus:     eventbus,
	}
}

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

func (a *NextgenApplier) AcceptVersionChange(verChange *ccent.ConfigurationVersionChange, changeType string) bool {
	return verChange != nil && changeType == string(api.ChangeTypeLogicalCluster)
}

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

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

func (a *NextgenApplier) acceptComponent(comp string) bool {
	return slices.Contains(ccproto.ClusterType_LOGICAL_CLUSTER.AvailableComponents(), comp)
}

func (a *NextgenApplier) acceptFile(configType string) bool {
	return slices.Contains(ccproto.ClusterType_LOGICAL_CLUSTER.AvailableConfigTypes(), configType)
}

func (a *NextgenApplier) Prepare(ctx context.Context, actx *ApplyContext) error {
	chg := actx.Input.Change
	if chg.TargetCluster.ClusterType != ccent.ClusterTypeLogicalCluster {
		return fmt.Errorf("unsupported cluster type %s for nextgen applier", chg.TargetCluster.ClusterType)
	}

	clusterIDInt, err := strconv.ParseUint(chg.TargetCluster.ClusterID, 10, 64)
	if err != nil {
		return fmt.Errorf("parse cluster id to int failed: %w", err)
	}
	cluster, err := a.coresvc.Mgmt.GetCluster(ctx, clusterIDInt)
	if err != nil {
		return fmt.Errorf("query cluster failed: %w", err)
	}
	// workaround: get logical region from resource pool
	resourcePool, err := a.coresvc.Mgmt.GetResourcePoolString(ctx, cluster.GetNextGenCluster().GetSharedPoolId())
	if err != nil {
		return fmt.Errorf("query resource pool failed: %w", err)
	}

	actx.NextGenApplierRequest = &NextGenApplierRequest{
		ClusterType:   cvt.To.ConfigCenter.ClusterType(chg.TargetCluster.ClusterType),
		ClusterId:     chg.TargetCluster.ClusterID,
		SharedPoolID:  cluster.GetNextGenCluster().SharedPoolId,
		LogicalRegion: resourcePool.GetRegion().GetLogicalName(),
		CloudProvider: resourcePool.GetRegion().GetCloudProviderId().String(),

		ClusterSvcReq: &clusterng.SetSharedPoolComponentConfigVersionRequest{
			SharedPoolId: cluster.GetNextGenCluster().SharedPoolId,
			Components:   []*clusterng.TargetComponent{}, // set later
			//Scope: ,
			//Mode:, set later
			Force: chg.ApplyPolicy == ccent.ParameterApplyPolicyForceRestart,
		},
		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.NextGenApplierRequest

	if actx.Input.Version != nil {
		request.CCRequest.Version = pointer.Pointer(actx.Input.Version.To)
	}

	// 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) error {
				//  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 = false // only modify, never create
				ccreq.ClearCustomParams = fileChg.ClearCustomParams

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

	for _, compChg := range actx.Input.Components {
		err := request.ModifyComponent(compChg.Component,
			func(ccReq *ccproto.UpdateClusterComponentRequest) error {
				if compChg.ModifiedVersion != nil {
					ccReq.Version = &compChg.ModifiedVersion.To
				} else 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
		}
	}

	err = a.setNextgenTargetComponents(ctx, request)
	if err != nil {
		return err
	}

	// set apply policy
	if chg.ApplyPolicy == ccent.ParameterApplyPolicyHotReload {
		request.ClusterSvcReq.Mode = clusterng.SetSharedPoolComponentConfigVersionRequest_HOT_RELOAD
	} else {
		request.ClusterSvcReq.Mode = clusterng.SetSharedPoolComponentConfigVersionRequest_ROLLING_UPDATE
	}
	// set scope
	if len(request.CCRequest.ComponentRequests) > 0 {
		if len(request.CCRequest.ConfigurationFileRequests) > 0 {
			request.ClusterSvcReq.Scope = clusterng.SetSharedPoolComponentConfigVersionRequest_ALL
		} else {
			request.ClusterSvcReq.Scope = clusterng.SetSharedPoolComponentConfigVersionRequest_VERSION
		}
	} else {
		if len(request.CCRequest.ConfigurationFileRequests) > 0 {
			request.ClusterSvcReq.Scope = clusterng.SetSharedPoolComponentConfigVersionRequest_CONFIG
		} else {
			return fmt.Errorf("no changes to apply for logical cluster")
		}
	}
	// check changeable
	if chg.ApplyPolicy != ccent.ParameterApplyPolicyForceRestart {
		sharedPool, err := a.mgmthttpcli.NextGen().GetSharedPool(ctx, request.ClusterId, request.SharedPoolID)
		if err != nil {
			return fmt.Errorf("get shared pool failed: %w", err)
		}
		targets := componentGroupAvailabilityTargets(request.CCRequest.GetClusterType(), request.ClusterSvcReq.Components)
		if len(targets) > 0 {
			done, reason := targetComponentsAvailable(sharedPool.ComponentGroups, targets)
			if !done {
				return fmt.Errorf("%s, not changeable", reason)
			}
		}
	}

	return nil
}

func (a *NextgenApplier) setNextgenTargetComponents(ctx context.Context, request *NextGenApplierRequest) error {
	// Logical-cluster changes are applied through the shared pool. Config-center
	// stores per-group TiDB config files as tidb-<tidb_group_id>, while the
	// shared-pool TiProxy target id is the premium logical cluster id.
	updatedTidbID := make(map[string]bool)
	updatedComponent := make(map[string]bool)
	for _, cfgreq := range request.CCRequest.ConfigurationFileRequests {
		if cfgreq.GetConfigurationType() == ccproto.LOGICAL_CLUSTER_CONFIG_TIDB {
			tidbID := strings.TrimPrefix(cfgreq.ConfigurationFilename, "tidb-")
			// filter default tidb group which is actually a template.
			if tidbID != "default" {
				updatedTidbID[tidbID] = true
			}
		} else {
			relatedComp := request.ClusterType.ConfigType2Component(cfgreq.GetConfigurationType())
			updatedComponent[relatedComp] = true
		}
	}
	for _, compreq := range request.CCRequest.ComponentRequests {
		updatedComponent[compreq.ComponentName] = true
		if compreq.ComponentName == ccproto.LOGICAL_CLUSTER_COMPONENT_TIDB {
			// mark all tidb group in this logical cluster as need update
			meta, err := a.mgmthttpcli.ConfigCenterNgProxy().GetClusterMeta(ctx, request.ClusterId, &ccproto.GetClusterMetaRequest{
				ClusterType: request.ClusterType,
				ClusterId:   request.ClusterId,
			})
			if err != nil {
				return fmt.Errorf("get cluster meta failed: %w", err)
			}
			for _, tidbConfigFile := range meta.GetConfigurationFiles() {
				if tidbConfigFile.GetConfigurationType() == ccproto.LOGICAL_CLUSTER_CONFIG_TIDB {
					tidbID := strings.TrimPrefix(tidbConfigFile.GetConfigurationFilename(), "tidb-")
					// filter default tidb group which is actually a template.
					if tidbID != "default" {
						updatedTidbID[tidbID] = true
					}
				}
			}
		}
	}

	for comp := range updatedComponent {
		switch comp {
		case ccproto.LOGICAL_CLUSTER_COMPONENT_TIDB:
			// tidb ApplyComponent must be added per tidb group, handle it later
			continue
		case ccproto.LOGICAL_CLUSTER_COMPONENT_DUMPLING:
			// dumpling only needs metadata update in config-center.
			continue
		case ccproto.LOGICAL_CLUSTER_COMPONENT_TIPROXY:
			request.ClusterSvcReq.Components = append(request.ClusterSvcReq.Components, &clusterng.TargetComponent{
				ComponentType: clusterng.ComponentType(clusterng.ComponentType_value[strings.ToUpper(comp)]),
				Id:            request.ClusterId, // in shared pool, tiproxy id is preimium cluster id
			})
		default:
			return fmt.Errorf("unsupported component %s for nextgen applier", comp)
		}
	}
	for tidbID := range updatedTidbID {
		applyComp := &clusterng.TargetComponent{
			ComponentType: clusterng.ComponentType_TIDB,
			Id:            tidbID,
		}
		request.ClusterSvcReq.Components = append(request.ClusterSvcReq.Components, applyComp)
	}

	return nil
}

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

	// 1. apply the file change via the config center through mgmt proxy
	if actx.ApplyMode.Appliable(api.ConfigCenterApply) {
		logger.Info("applying cluster config change via config center mgmt proxy", "req", protojson.Format(request.CCRequest))
		_, err := a.mgmthttpcli.ConfigCenterNgProxy().UpdateClusterMeta(ctx, request.ClusterId, 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")
	}

	// 2. apply the change via the cluster-service-ng through mgmt proxy
	if actx.ApplyMode.Appliable(api.MgmtServiceApply) {
		if len(request.ClusterSvcReq.Components) == 0 {
			logger.Info("skip apply via cluster-service-ng because no components require rollout")
			return nil
		}
		// request.MgmtRequest.StatusId = setStatusResp.StatusId
		reqJSON, _ := json.Marshal(request.ClusterSvcReq) // for debug
		logger.Info("applying cluster config change via mgmt http", "req", string(reqJSON))
		err := a.mgmthttpcli.NextGenRegionServer(request.LogicalRegion, request.CloudProvider).SetSharedPoolComponentConfigVersion(ctx, request.ClusterSvcReq)
		if err != nil {
			return fmt.Errorf("apply cluster config via cluster-service-ng failed: %w", err)
		}
	} else {
		logger.Info("skip apply via cluster-service-ng cause apply method missed in apply mode")
	}

	// send event
	clusterIDUint, _ := strconv.ParseUint(request.ClusterId, 10, 64)
	cluster, err := a.coresvc.Mgmt.GetCluster(ctx, clusterIDUint)
	if err != nil {
		logger.Error(err, "query cluster failed when publishing config change apply event", "cluster_id", request.ClusterId)
	} else {
		displayEv := &evbus.DisplayEvent{
			DisplayEventType: string(eventent.ConfigChangeApplyEvent),
			DisplayStatus:    evbus.EventStatusRunning,
			Severity:         evbus.EventSeverityInfo,
			Summary:          string(eventent.ConfigChangeApplySummary),
			HappenedAtMs:     uint64(time.Now().UnixMilli()),
			Author:           "",
			TenantID:         cluster.OrganizationId,
			ProjectID:        0,
			ClusterID:        clusterIDUint,
			Detail:           protojson.Format(request.CCRequest),
		}
		err = a.eventBus.DBaaSDisplayEvent.Publish(displayEv)
		if err != nil {
			logger.Error(err, "publish config change apply event failed", "cluster_id", request.ClusterId)
		}
	}

	return nil
}

func (a *NextgenApplier) 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)
	}

	targets, ok := a.nextgenCheckResultTargets(ctx, rctx.Input, cluster)
	if ok {
		sharedPool, err := a.mgmthttpcli.NextGenRegionServer(targets.LogicalRegion, targets.CloudProvider).GetSharedPool(ctx, targets.SharedPoolID)
		if err != nil {
			return false, "", fmt.Errorf("get shared pool failed: %w", err)
		}
		done, reason := targetComponentsAvailable(sharedPool.ComponentGroups, targets.Components)
		if done {
			err := a.publishNextgenFinishEvent(ctx, cluster.OrganizationId, clusterIDInt)
			if err != nil {
				logger := log.FromContext(ctx).WithName(a.Name())
				logger.Error(err, "publish config change finish event failed", "cluster_id", clusterIDInt)
			}
			return true, "", nil
		}
		return false, reason, nil
	}

	state := cluster.GetNextGenCluster().GetState()
	if state == nextgenv1.Cluster_AVAILABLE {
		// send event
		err = a.publishNextgenFinishEvent(ctx, cluster.OrganizationId, clusterIDInt)
		if err != nil {
			logger := log.FromContext(ctx).WithName(a.Name())
			logger.Error(err, "publish config change finish event failed", "cluster_id", clusterIDInt)
		}
		return true, "", nil
	}

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

type sharedPoolTargetComponents struct {
	SharedPoolID  string
	LogicalRegion string
	CloudProvider string
	Components    []*clusterng.TargetComponent
}

func (a *NextgenApplier) nextgenCheckResultTargets(ctx context.Context, input *Input, cluster *mgmtv1.Cluster) (*sharedPoolTargetComponents, bool) {
	if input == nil || input.Version != nil {
		return nil, false
	}
	nextgenCluster := cluster.GetNextGenCluster()
	if nextgenCluster == nil {
		return nil, false
	}

	resourcePool, err := a.coresvc.Mgmt.GetResourcePoolString(ctx, nextgenCluster.GetSharedPoolId())
	if err != nil {
		return nil, false
	}
	logicalRegion, cloudProvider, ok := resourcePoolRegion(resourcePool)
	if !ok {
		return nil, false
	}

	request := &NextGenApplierRequest{
		ClusterType:   ccproto.ClusterType_LOGICAL_CLUSTER,
		ClusterId:     strconv.FormatUint(cluster.GetClusterId(), 10),
		SharedPoolID:  nextgenCluster.GetSharedPoolId(),
		LogicalRegion: logicalRegion,
		CloudProvider: cloudProvider,
		ClusterSvcReq: &clusterng.SetSharedPoolComponentConfigVersionRequest{
			SharedPoolId: nextgenCluster.GetSharedPoolId(),
			Components:   []*clusterng.TargetComponent{},
		},
		CCRequest: inputClusterMetaRequest(ccproto.ClusterType_LOGICAL_CLUSTER, strconv.FormatUint(cluster.GetClusterId(), 10), input),
	}
	if err := a.setNextgenTargetComponents(ctx, request); err != nil {
		return nil, false
	}
	targets := componentGroupAvailabilityTargets(request.CCRequest.GetClusterType(), request.ClusterSvcReq.Components)

	return &sharedPoolTargetComponents{
		SharedPoolID:  request.SharedPoolID,
		LogicalRegion: request.LogicalRegion,
		CloudProvider: request.CloudProvider,
		Components:    targets,
	}, targetComponentsReliable(targets)
}

func (a *NextgenApplier) publishNextgenFinishEvent(ctx context.Context, tenantID, clusterID uint64) error {
	displayEv := &evbus.DisplayEvent{
		DisplayEventType: string(eventent.ConfigChangeFinishEvent),
		DisplayStatus:    evbus.EventStatusSucceeded,
		Severity:         evbus.EventSeverityInfo,
		Summary:          string(eventent.ConfigChangeFinishSummary),
		HappenedAtMs:     uint64(time.Now().UnixMilli()),
		Author:           "",
		TenantID:         tenantID,
		ProjectID:        0,
		ClusterID:        clusterID,
	}
	return a.eventBus.DBaaSDisplayEvent.Publish(displayEv)
}
