package entity

import (
	"context"
	"math"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"

	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"
)

const (
	DefaultRaftLogDirPath   = "/var/lib/raft/data"
	DefaultRaftLogSpillPath = "/var/lib/tikv/raft-spill"
	DefaultMinRcu           = 5000
)

type (
	Cluster struct {
		*globalngv1.Cluster

		ProjectId                   uint64
		EncryptionKeyId             uint64
		RegionId                    uint64
		CloudProviderId             uint64
		Versioning                  int
		ProxyBaseUrl                string // The base URL for the cluster proxy, used to access the cluster.
		EncryptedCloudAdminPassword string
		CapacityUpdatedAt           *time.Time // Last time MaxRcu was modified
		PlanReplayerExpireAt        *time.Time
	}

	State       = globalngv1.Cluster_State
	HealthState = globalngv1.Cluster_HealthState

	// ProviderRole will be replaced by RegionProviderIdentity
	ProviderRole = globalngv1.ProviderRole
)

func (c *Cluster) CreateEvent(ctx context.Context) ([]clusterevent.Event, bizerr.BizError) {
	if c.State != globalngv1.Cluster_CREATING {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "invalid status, status=%s", c.State.String())
	}

	creatingEvent, err := NewClusterEvent(ctx, c, clusterevent.EventTypeCreating, "CLUSTER_STATUS_INITIALIZING")
	if err != nil {
		return nil, err
	}

	initializingEvent, err := NewClusterEvent(ctx, c, clusterevent.EventTypeInitializing, "CLUSTER_STATUS_PREPARING")
	if err != nil {
		return nil, err
	}

	events := []clusterevent.Event{creatingEvent, initializingEvent}

	// For Premium/BYOC clusters, also emit a resource pool event so that the resource pool's
	// event timeline records the instance being created on it.
	if c.isSharedPoolInstance() {
		rpEvent, rpErr := NewResourcePoolClusterInstanceEvent(ctx,
			c,
			clusterevent.EventTypeInstanceCreatingOnResourcePool,
			"CLUSTER_STATUS_INITIALIZING")
		if rpErr != nil {
			return nil, rpErr
		}
		events = append(events, rpEvent)
	}

	return events, nil
}

func (c *Cluster) DeleteEvent(ctx context.Context) ([]clusterevent.Event, bizerr.BizError) {
	deletingEvent, err := NewClusterEvent(ctx, c, clusterevent.EventTypeDeleting, "CLUSTER_STATUS_DELETED")
	if err != nil {
		return nil, err
	}

	deletedEvent, err := NewClusterEvent(ctx, c, clusterevent.EventTypeDeleted, "CLUSTER_STATUS_DELETED")
	if err != nil {
		return nil, err
	}

	events := []clusterevent.Event{deletingEvent, deletedEvent}

	// For Premium/BYOC clusters, also emit resource pool events so that the resource pool's
	// event timeline records the instance being removed from it.
	if c.isSharedPoolInstance() {
		rpDeletingEvent, rpErr := NewResourcePoolClusterInstanceEvent(ctx,
			c,
			clusterevent.EventTypeInstanceDeletingOnResourcePool,
			"CLUSTER_STATUS_DELETED")
		if rpErr != nil {
			return nil, rpErr
		}
		rpDeletedEvent, rpErr := NewResourcePoolClusterInstanceEvent(ctx,
			c,
			clusterevent.EventTypeInstanceDeletedOnResourcePool,
			"CLUSTER_STATUS_DELETED")
		if rpErr != nil {
			return nil, rpErr
		}
		events = append(events, rpDeletingEvent, rpDeletedEvent)
	}

	return events, nil
}

func (c *Cluster) ReadyEvent(ctx context.Context) ([]clusterevent.Event, bizerr.BizError) {
	if c.CreateTime == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster create time cannot be nil")
	}

	duration := time.Since(c.CreateTime.AsTime())
	normalizedEv, err := c.normalize(ctx, duration)
	if err != nil {
		return nil, err
	}

	createdEv, err := NewClusterEvent(ctx, c, clusterevent.EventTypeCreated, "CLUSTER_STATUS_NORMAL")
	if err != nil {
		return nil, err
	}
	createdEv.ExtraInfo.DurationSeconds = proto.Int64(int64(duration.Seconds()))

	events := []clusterevent.Event{createdEv, normalizedEv}

	// For Premium/BYOC clusters, also emit a resource pool event so that the resource pool's
	// event timeline records the instance as successfully created on it.
	if c.isSharedPoolInstance() {
		rpEvent, rpErr := NewResourcePoolClusterInstanceEvent(ctx, c, clusterevent.EventTypeInstanceCreatedOnResourcePool, "CLUSTER_STATUS_NORMAL")
		if rpErr != nil {
			return nil, rpErr
		}
		events = append(events, rpEvent)
	}

	return events, nil
}

func (c *Cluster) normalize(ctx context.Context, duration time.Duration) (*clusterevent.ClusterEvent, bizerr.BizError) {
	normalizedEv, err := NewClusterEvent(ctx, c, clusterevent.EventTypeNormal, "CLUSTER_STATUS_NORMAL")
	if err != nil {
		return nil, err
	}
	normalizedEv.ExtraInfo.DurationSeconds = proto.Int64(int64(duration.Seconds()))
	return normalizedEv, nil
}

// CalculateRealMaxRcuWithBurstyRatio calculates the real max RCU with bursty ratio.
// If burstyRatio <= 1, return maxRcu directly.
// If burstyRatio > 1, return maxRcu * burstyRatio, rounded up to the nearest integer.
// If the result exceeds math.MaxUint32, return math.MaxUint32.
func (c *Cluster) CalculateRealMaxRcuWithBurstyRatio(ctx context.Context, burstyRatio float32) uint32 {
	if burstyRatio <= 1 {
		return uint32(c.MaxRcu)
	}
	const precision = 1000
	ratioInt := uint64(burstyRatio * precision)

	uint64Value := ((uint64(c.MaxRcu))*ratioInt + precision - 1) / precision
	if uint64Value >= math.MaxUint32 {
		tilog.Error(ctx, "calculated real max RCU exceeds max uint32",
			zap.Int64("max_rcu", c.MaxRcu),
			zap.Float32("bursty_ratio", burstyRatio),
			zap.Uint64("calculated_value", uint64Value))
		return math.MaxUint32
	}
	return uint32(uint64Value)
}

// isSharedPoolInstance returns true for Premium and BYOC clusters that belong to a resource pool.
// Only these cluster types need to emit resource pool instance events on creation/deletion.
func (c *Cluster) isSharedPoolInstance() bool {
	return (c.GetServicePlan() == globalcommonv1.ServicePlan_Premium ||
		c.GetServicePlan() == globalcommonv1.ServicePlan_BYOC ||
		c.GetServicePlan() == globalcommonv1.ServicePlan_Premium_Reserved) &&
		c.GetSharedPoolId() != ""
}
