package entity

import (
	"context"
	"strconv"
	"time"

	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

type ClusterEventBuilder struct {
	ev *clusterevent.ClusterEvent
}

func NewClusterEventBuilder(ctx context.Context, c *Cluster, eventType clusterevent.EventType) *ClusterEventBuilder {
	var (
		operatorId       uint64
		email            string
		openAPIPublicKey string
	)

	reqMeta, err := metadata.ExtractRequestMeta(ctx)
	if err != nil {
		tilog.Warn(ctx, "extract request meta for cluster event", zap.Error(err))
	} else {
		operatorId, err = strconv.ParseUint(reqMeta.UserID, 10, 64)
		if err != nil {
			tilog.Warn(ctx, "parse uint failed", zap.String("id", reqMeta.UserID), zap.Error(err))
		}
		email = reqMeta.Email
		openAPIPublicKey = reqMeta.ApiPublicKey
	}

	// Set default topology info.
	tp := BuildClusterTopology(c, 0)

	builder := &ClusterEventBuilder{
		ev: &clusterevent.ClusterEvent{
			TenantID:         c.OrganizationId,
			ProjectID:        c.ProjectId,
			ClusterID:        c.ClusterId,
			TraceID:          clusterevent.TraceIDFromContext(ctx),
			OperatorID:       operatorId,
			UserEmail:        email,
			OpenAPIPublicKey: openAPIPublicKey,
			IsDevTier:        false,
			Status:           c.State.String(),
			EventType:        eventType,
			EventCreatedAtMs: time.Now().UnixMilli(),
			ClusterType:      clusterevent.ClusterTypeDedicatedCluster,
			ExtraInfo: &clusterevent.EventExtraInfo{
				TopologyBefore: tp,
				TopologyAfter:  tp,
			},
		},
	}

	return builder
}

func (b *ClusterEventBuilder) SetDuration(durationSeconds float64) *ClusterEventBuilder {
	b.ev.ExtraInfo.DurationSeconds = lo.ToPtr(int64(durationSeconds))
	return b
}

func (b *ClusterEventBuilder) SetTopologyBefore(topology *clusterevent.ClusterTopology) *ClusterEventBuilder {
	b.ev.ExtraInfo.TopologyBefore = topology
	return b
}

func (b *ClusterEventBuilder) SetTopologyAfter(topology *clusterevent.ClusterTopology) *ClusterEventBuilder {
	b.ev.ExtraInfo.TopologyAfter = topology
	return b
}

func (b *ClusterEventBuilder) SetOperationContext(operationContext *clusterevent.OperationContext) *ClusterEventBuilder {
	b.ev.ExtraInfo.OperationContext = operationContext
	return b
}

func (b *ClusterEventBuilder) SetOldVersion(oldVersion string) *ClusterEventBuilder {
	b.ev.ExtraInfo.OldVersion = &oldVersion
	return b
}

func (b *ClusterEventBuilder) SetNewVersion(newVersion string) *ClusterEventBuilder {
	b.ev.ExtraInfo.NewVersion = &newVersion
	return b
}

func (b *ClusterEventBuilder) SetPausePlanType(pausePlanType string) *ClusterEventBuilder {
	b.ev.ExtraInfo.PausePlanType = pausePlanType
	return b
}

func (b *ClusterEventBuilder) Build() *clusterevent.ClusterEvent {
	return b.ev
}

func BuildClusterTopology(cluster *Cluster, defaultTidbGroupId uint64) *clusterevent.ClusterTopology {
	exTidbNodeGroupEvents := make([]*clusterevent.TidbNodeGroup, 0) // without default tidb group
	defaultTidbGroupComponents := make([]*globaldedicatedv1.Cluster_ClusterComponent, 0)

	lo.ForEach(cluster.GetClusterComponents(), func(item *globaldedicatedv1.Cluster_ClusterComponent, index int) {
		if item.GetTidbGroupId() == defaultTidbGroupId {
			// default tidb group
			defaultTidbGroupComponents = append(defaultTidbGroupComponents, item)
		} else if item.GetComponentType() == globaldedicatedv1.ComponentType_TIDB {
			// only add tidb and tiproxy
			exTidb := &clusterevent.TidbNodeGroup{
				ID:               item.GetTidbGroupId(),
				DisplayName:      item.GetTidbGroupDisplayName(),
				TidbNodeQuantity: item.GetCount(),
			}

			tiProxy, ok := lo.Find(cluster.GetClusterComponents(), func(each *globaldedicatedv1.Cluster_ClusterComponent) bool {
				return each.GetTidbGroupId() == item.GetTidbGroupId() && each.GetComponentType() == globaldedicatedv1.ComponentType_TIPROXY
			})

			if ok && tiProxy.GetCount() > 0 {
				exTidb.TiProxyQuantity = tiProxy.GetCount()
				exTidb.TiProxySpuName = tiProxy.GetSpuName()
				exTidb.TiProxySpuDisplayName = tiProxy.GetSpuDisplayName()
			}
			exTidbNodeGroupEvents = append(exTidbNodeGroupEvents, exTidb)
		}
	})

	out := &clusterevent.ClusterTopology{
		Components:       map[string]*clusterevent.ClusterComponent{},
		ExTidbNodeGroups: exTidbNodeGroupEvents,
	}
	for _, each := range defaultTidbGroupComponents {
		tp := each.GetComponentType().String()
		dataStorage := each.GetStorages()[globaldedicatedv1.Sku_DATA.String()]
		raftLogStorage := each.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()]
		out.Components[tp] = &clusterevent.ClusterComponent{
			ComponentType:     tp,
			SpuName:           each.GetSpuName(),
			SpuDisplayName:    each.GetSpuDisplayName(),
			SpuKey:            each.GetSpuKey(),
			SkuKey:            each.GetSkuKey(),
			Quantity:          each.GetCount(),
			StorageSizeGi:     dataStorage.GetSizeGi(),
			StorageIops:       dataStorage.GetIops(),
			StorageThroughput: dataStorage.GetThroughputMibps(),
			AdditionalIops:    dataStorage.GetAdditionalIops(),
			VolumeType:        dataStorage.GetVolumeType().String(),
			RaftLogVolumeType: raftLogStorage.GetVolumeType().String(),
			RaftLogIops:       raftLogStorage.GetIops(),
			RaftLogThroughput: raftLogStorage.GetThroughputMibps(),
		}
	}

	return out
}

func BuildScaleOperationContext(
	sourceOperation string,
	requestedFields []string,
	clusterComponents []*globaldedicatedv1.Cluster_ClusterComponent,
) *clusterevent.OperationContext {
	if sourceOperation == "" && len(requestedFields) == 0 {
		return nil
	}

	return &clusterevent.OperationContext{
		SourceOperation:     sourceOperation,
		RequestedFields:     requestedFields,
		RequestedComponents: BuildRequestedClusterComponents(clusterComponents),
	}
}

func BuildRequestedClusterComponents(
	clusterComponents []*globaldedicatedv1.Cluster_ClusterComponent,
) []*clusterevent.RequestedClusterComponent {
	return lo.Map(clusterComponents, func(component *globaldedicatedv1.Cluster_ClusterComponent, _ int) *clusterevent.RequestedClusterComponent {
		requested := &clusterevent.RequestedClusterComponent{
			ComponentType:        component.GetComponentType().String(),
			TidbGroupID:          component.GetTidbGroupId(),
			TidbGroupDisplayName: component.GetTidbGroupDisplayName(),
			SpuName:              component.GetSpuName(),
			SpuDisplayName:       component.GetSpuDisplayName(),
			SpuKey:               component.GetSpuKey(),
			SkuKey:               component.GetSkuKey(),
			Storages:             map[string]*clusterevent.RequestedStorage{},
		}
		if component.Count != nil {
			requested.Quantity = lo.ToPtr(component.GetCount())
		}
		for diskType, storage := range component.GetStorages() {
			requested.Storages[diskType] = &clusterevent.RequestedStorage{
				DiskType:        storage.GetDiskType().String(),
				SizeGi:          storage.GetSizeGi(),
				Iops:            storage.GetIops(),
				ThroughputMibps: storage.GetThroughputMibps(),
				VolumeType:      storage.GetVolumeType().String(),
				AdditionalIops:  storage.GetAdditionalIops(),
			}
		}
		if len(requested.Storages) == 0 {
			requested.Storages = nil
		}
		return requested
	})
}

func BuildApplyOperationContext(
	applyMask *globaldedicatedv1.ClusterApplyMask,
	operations map[string][]string,
) *clusterevent.OperationContext {
	return &clusterevent.OperationContext{
		SourceOperation:     string(clusterevent.OperationSourceApplyCluster),
		RequestedFields:     BuildApplyRequestedFields(applyMask),
		ComponentOperations: operations,
		ApplyMask:           BuildClusterApplyMask(applyMask),
	}
}

func BuildApplyRequestedFields(applyMask *globaldedicatedv1.ClusterApplyMask) []string {
	if applyMask == nil {
		return nil
	}

	if applyMask.GetVersion() {
		return []string{"version"}
	}

	out := make([]string, 0)
	for _, componentMask := range applyMask.GetComponentApplyMasks() {
		componentType := componentMask.GetComponentType().String()
		if componentMask.GetConfiguration() {
			out = append(out, componentType+".configuration")
		}
		if componentMask.GetImage() {
			out = append(out, componentType+".image")
		}
	}
	return out
}

func BuildClusterApplyMask(applyMask *globaldedicatedv1.ClusterApplyMask) *clusterevent.ClusterApplyMask {
	if applyMask == nil {
		return nil
	}

	return &clusterevent.ClusterApplyMask{
		Version:   applyMask.GetVersion(),
		Strategy:  applyMask.GetStrategy().String(),
		HotReload: applyMask.GetHotReload(),
		ComponentApplyMasks: lo.Map(
			applyMask.GetComponentApplyMasks(),
			func(componentMask *globaldedicatedv1.ClusterApplyMask_ComponentApplyMask, _ int) *clusterevent.ComponentApplyMask {
				return &clusterevent.ComponentApplyMask{
					ComponentType: componentMask.GetComponentType().String(),
					Configuration: componentMask.GetConfiguration(),
					Image:         componentMask.GetImage(),
				}
			},
		),
	}
}
