package entity

import (
	"context"
	"encoding/json"
	"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"
	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
)

type Cluster struct {
	globalserverlessv1.ServerlessServiceCluster

	EncryptedCloudAdminPassword string
	EncryptedRootPassword       string

	Region        *Region
	CloudProvider *CloudProvider

	CreationFinishedAt *time.Time
	DeletedAt          time.Time
	CreatedAt          time.Time // immutable
	UpdatedAt          time.Time

	Versioning int

	Version string

	// some attributes of the cluster, such as: tokenBasedAuthEnabled
	Attributes ClusterAttributes

	ServicePlan globalcommonv1.ServicePlan
	EngineType  string

	ExternalInstanceID string
}

type ClusterAttributes struct {
	// TokenBasedAuthEnabled whether enable the delegate user for this cluster
	TokenBasedAuthEnabled bool

	// WithBuiltinCDCAdminRole indicates whether this cluster has the 'role_cdc_admin' DB role
	WithBuiltinCDCAdminRole bool
}

func (attr *ClusterAttributes) String() string {
	data, _ := json.Marshal(attr)
	return string(data)
}

func (c *Cluster) CreateEvent(ctx context.Context) ([]clusterevent.Event, error) {
	if c.UserStatus != globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_CREATING {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "invalid status, status=%s", c.UserStatus.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
	}

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

func (c *Cluster) DeleteEvent(ctx context.Context) ([]clusterevent.Event, error) {
	if !c.DeletedAt.IsZero() {
		return nil, bizerr.New(bizerr.ErrCodeParams, "can't delete deleted cluster")
	}

	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
	}

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

func (c *Cluster) ReadyEvent(ctx context.Context) ([]clusterevent.Event, error) {
	duration := time.Since(c.CreatedAt)
	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()))

	return []clusterevent.Event{createdEv, normalizedEv}, nil
}

func (c *Cluster) normalize(ctx context.Context, duration time.Duration) (*clusterevent.ClusterEvent, error) {
	c.UserStatus = globalserverlessv1.UserClusterStatus_USER_CLUSTER_STATUS_NORMAL

	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
}

func IsReady4Launch(ctx context.Context, cluster *regionalserverlessv1.ServerlessRegionalServiceClusterInfo) bool {
	if cluster.State != regionalserverlessv1.ClusterState_CLUSTER_STATE_READY {
		tilog.Warn(ctx, "wait for cluster ready", zap.String("cluster", cluster.ClusterId))
		return false
	}

	if cluster.Endpoints == nil || cluster.Endpoints.Public == nil ||
		(!cluster.Endpoints.Public.Disabled && cluster.Endpoints.Public.Host == "") {
		tilog.Warn(ctx, "can not get public endpoint from infra api", zap.String("cluster", cluster.ClusterId))
		return false
	}

	if cluster.ProxyEndpoint == "" {
		tilog.Warn(ctx, "proxy endpoint not ready", zap.String("cluster", cluster.ClusterId))
		return false
	}

	if cluster.Version == "" {
		tilog.Warn(ctx, "version of cluster is unknown", zap.String("cluster", cluster.ClusterId))
		return false
	}

	return true
}
