package entity

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

type ResourcePoolEventAggregation struct {
	*ResourcePool
	*globalngv1.CreateResourcePoolRequest

	*clusterngv1beta.SharedPool
	*clusterngv1beta.CreateSharedPoolRequest
}

func (e *ResourcePoolEventAggregation) CreateEvent(ctx context.Context) ([]clusterevent.Event, bizerr.BizError) {
	creatingEvent, err := NewResourcePoolClusterEvent(ctx, e, clusterevent.EventTypeCreating, "CLUSTER_STATUS_INITIALIZING", nil)
	if err != nil {
		return nil, err
	}

	return []clusterevent.Event{creatingEvent}, nil
}

func (e *ResourcePoolEventAggregation) ReadyEvent(ctx context.Context) ([]clusterevent.Event, bizerr.BizError) {
	extraInfo := &clusterevent.EventExtraInfo{
		ResourcePoolTopology: buildResourcePoolTopology(e.SharedPool, e.ServicePlan),
	}
	readyEvent, err := NewResourcePoolClusterEvent(ctx, e, clusterevent.EventTypeCreated, "CLUSTER_STATUS_NORMAL", extraInfo)
	if err != nil {
		return nil, err
	}

	return []clusterevent.Event{readyEvent}, nil
}

// DeletingEvent builds the "start to delete" event for a resource pool. It is
// intended to be published at the beginning of a delete operation, within the
// same DB transaction as the soft-delete.
func (e *ResourcePoolEventAggregation) DeletingEvent(ctx context.Context) ([]clusterevent.Event, bizerr.BizError) {
	deletingEvent, err := NewResourcePoolClusterEvent(ctx, e, clusterevent.EventTypeDeleting, "CLUSTER_STATUS_DELETING", nil)
	if err != nil {
		return nil, err
	}

	return []clusterevent.Event{deletingEvent}, nil
}

// DeleteEvent builds the "delete succeeded" event. It is intended to be
// published after the resource pool has been successfully removed from the
// regional server.
func (e *ResourcePoolEventAggregation) DeleteEvent(ctx context.Context) ([]clusterevent.Event, bizerr.BizError) {
	deletedEvent, err := NewResourcePoolClusterEvent(ctx, e, clusterevent.EventTypeDeleted, "CLUSTER_STATUS_DELETED", nil)
	if err != nil {
		return nil, err
	}

	return []clusterevent.Event{deletedEvent}, nil
}

// NewResourcePoolLifecycleEvent builds a pause/resume lifecycle event for a resource pool.
func NewResourcePoolLifecycleEvent(
	ctx context.Context,
	resourcePool *ResourcePool,
	eventType clusterevent.EventType,
) (*clusterevent.ClusterEvent, bizerr.BizError) {
	if resourcePool == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource pool cannot be nil")
	}

	var status string
	switch eventType {
	case clusterevent.EventTypePausing:
		status = "CLUSTER_STATUS_PAUSING"
	case clusterevent.EventTypePaused:
		status = "CLUSTER_STATUS_PAUSED"
	case clusterevent.EventTypeResuming:
		status = "CLUSTER_STATUS_RESUMING"
	case clusterevent.EventTypeResumed:
		status = "CLUSTER_STATUS_NORMAL"
	default:
		return nil, bizerr.New(bizerr.ErrCodeParams, "unsupported resource pool lifecycle event type: %s", eventType)
	}

	return newResourcePoolEvent(ctx, resourcePool, eventType, status, nil)
}

// NewResourcePoolSetDefaultEvent builds a ResourcePoolDefaultSet cluster event.
func NewResourcePoolSetDefaultEvent(
	ctx context.Context,
	resourcePool *ResourcePool,
	cloud string,
	region string,
) (*clusterevent.ClusterEvent, bizerr.BizError) {
	if resourcePool == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource pool cannot be nil")
	}
	return newResourcePoolEvent(ctx, resourcePool,
		clusterevent.EventTypeResourcePoolDefaultSet,
		"RESOURCE_POOL_STATUS_NORMAL",
		&clusterevent.EventExtraInfo{
			Cloud:  cloud,
			Region: region,
		},
	)
}

// NewResourcePoolInstanceLimitUpdatedEvent builds a ResourcePoolInstanceLimitUpdated event.
func NewResourcePoolInstanceLimitUpdatedEvent(
	ctx context.Context,
	resourcePool *ResourcePool,
	limitBefore uint64,
	limitAfter uint64,
) (*clusterevent.ClusterEvent, bizerr.BizError) {
	if resourcePool == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource pool cannot be nil")
	}
	return newResourcePoolEvent(ctx, resourcePool,
		clusterevent.EventTypeResourcePoolInstanceLimitUpdated,
		"RESOURCE_POOL_STATUS_NORMAL",
		&clusterevent.EventExtraInfo{
			InstanceLimitBefore: &limitBefore,
			InstanceLimitAfter:  &limitAfter,
		},
	)
}

// newResourcePoolEvent is the shared constructor for all resource pool cluster events.
func newResourcePoolEvent(
	ctx context.Context,
	resourcePool *ResourcePool,
	eventType clusterevent.EventType,
	status string,
	extraInfo *clusterevent.EventExtraInfo,
) (*clusterevent.ClusterEvent, bizerr.BizError) {
	var (
		operatorId       uint64
		email            string
		openAPIPublicKey string
	)

	reqMeta, err := metadata.ExtractRequestMeta(ctx)
	if err != nil {
		tilog.Warn(ctx, "extract request meta for resource pool 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
	}

	if extraInfo == nil {
		extraInfo = &clusterevent.EventExtraInfo{}
	}

	return &clusterevent.ClusterEvent{
		TenantID:         resourcePool.OwnerId,
		ProjectID:        0,
		ClusterID:        resourcePool.GetId(),
		TraceID:          clusterevent.TraceIDFromContext(ctx),
		OperatorID:       operatorId,
		UserEmail:        email,
		OpenAPIPublicKey: openAPIPublicKey,
		IsDevTier:        false,
		Status:           status,
		EventType:        eventType,
		EventCreatedAtMs: time.Now().UnixMilli(),
		ClusterType:      clusterevent.ClusterTypeTidbxResourcePool,
		ExtraInfo:        extraInfo,
		ServicePlan:      resourcePool.ServicePlan.String(),
	}, nil
}

// buildResourcePoolTopology builds a human-readable topology string from the
// SharedPool ComponentGroups using the component type and storage type fields
// that are confirmed available in the proto.
func buildResourcePoolTopology(sharedPool *clusterngv1beta.SharedPool, servicePlan globalcommonv1.ServicePlan) string {
	if sharedPool == nil {
		return ""
	}

	includeSpecAndCount := servicePlan == globalcommonv1.ServicePlan_Premium_Reserved

	var parts []string
	for _, cg := range sharedPool.GetComponentGroups() {
		if cg == nil {
			continue
		}
		compName := resourcePoolComponentTypeName(cg.ComponentType)
		nodeQuota := cg.GetNodeQuota()
		nodeSpec := ""
		nodeCount := uint32(0)
		if nodeQuota != nil {
			nodeCount = nodeQuota.GetNodeCount()
			if spec := nodeQuota.GetNodeSpec(); spec != nil {
				nodeSpec = fmt.Sprintf("%dC%dG", spec.GetCpu(), spec.GetMemoryGi())
			}
		}
		var storageInfos []string
		for _, s := range cg.GetStorages() {
			if s == nil {
				continue
			}
			if s.GetType() != "" {
				storageInfos = append(storageInfos, fmt.Sprintf("%s: %s", s.GetName(), s.GetType()))
			}
		}
		storageStr := strings.Join(storageInfos, ", ")

		title := compName
		if includeSpecAndCount {
			if nodeCount > 0 {
				title = fmt.Sprintf("%s x%d", title, nodeCount)
			}
		}

		attrs := make([]string, 0, 2)
		if includeSpecAndCount && nodeSpec != "" {
			attrs = append(attrs, nodeSpec)
		}
		if storageStr != "" {
			attrs = append(attrs, storageStr)
		}
		if len(attrs) > 0 {
			parts = append(parts, fmt.Sprintf("%s (%s)", title, strings.Join(attrs, ", ")))
		} else {
			parts = append(parts, title)
		}
	}

	if len(parts) == 0 {
		return ""
	}
	return "{" + strings.Join(parts, ", ") + "}"
}

// resourcePoolComponentTypeName maps a ComponentType enum to its display name.
func resourcePoolComponentTypeName(ct clusterngv1beta.ComponentType) string {
	switch ct {
	case clusterngv1beta.ComponentType_TIKV:
		return "TiKV"
	case clusterngv1beta.ComponentType_TIFLASH_WRITE:
		return "TiFlash Write"
	case clusterngv1beta.ComponentType_TIFLASH_COMPUTE:
		return "TiFlash Compute"
	default:
		return ct.String()
	}
}

// NewResourcePoolClusterEvent is kept for backward-compatible call sites that
// already have a ResourcePoolEventAggregation.
func NewResourcePoolClusterEvent(
	ctx context.Context,
	r *ResourcePoolEventAggregation,
	eventType clusterevent.EventType,
	status string,
	extraInfo *clusterevent.EventExtraInfo,
) (*clusterevent.ClusterEvent, bizerr.BizError) {
	if r == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "resource pool aggregation cannot be nil")
	}
	return newResourcePoolEvent(ctx, r.ResourcePool, eventType, status, extraInfo)
}
