package common

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

	"github.com/hashicorp/go-multierror"
	opv2 "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
	"github.com/tidbcloud/config-service/sdk/api"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	"github.com/tidbcloud/eks-provider/controllers/cluster/task/common/builder"
	"github.com/tidbcloud/eks-provider/controllers/cluster/task/common/feature"
	"github.com/tidbcloud/eks-provider/controllers/cluster/task/common/status"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	sdk "github.com/tidbcloud/infra-provider/pkg/gitops-sdk"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

type TiDBGroupV2 struct {
	*task.BaseTask
}

func NewTiDBGroupV2(ctx *task.Context) deploy.Task {
	return &TiDBGroupV2{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

func (t *TiDBGroupV2) Name() string {
	return "TiDBGroupV2"
}

func (t *TiDBGroupV2) Create(ctx context.Context) (res deploy.Result) {
	t.BeginAt = ptr.To(time.Now())
	if len(t.Cluster.Spec.ExtraGroups) == 0 {
		return deploy.Done()
	}

	extraGroups := t.Cluster.GetExtraGroups()

	cfg, err := t.ClusterConfig()
	if err != nil {
		return deploy.Error(err)
	}
	gates := feature.Init(t.Cluster, t.Config.Provider, t.GetNodeVersion(true), cfg.Version)

	var errs *multierror.Error
	for _, extraGroup := range extraGroups {
		if extraGroup.Deleted() {
			errs = multierror.Append(errs, t.delete(ctx, &extraGroup))
			continue
		}
		errs = multierror.Append(errs, t.create(ctx, gates, cfg, &extraGroup))
	}
	return deploy.ErrorOrDone(errs.ErrorOrNil())
}

func (t *TiDBGroupV2) PostCreate(ctx context.Context) deploy.Result {
	if len(t.Cluster.Spec.ExtraGroups) == 0 {
		return deploy.Done()
	}

	extraGroups := t.Cluster.GetExtraGroups()
	cfg, err := t.ClusterConfig()
	if err != nil {
		return deploy.Error(err)
	}
	gates := feature.Init(t.Cluster, t.Config.Provider, t.GetNodeVersion(true), cfg.Version)

	var errs *multierror.Error

	for _, extraGroup := range extraGroups {
		if extraGroup.Deleted() {
			continue
		}
		errs = multierror.Append(errs, t.observe(ctx, gates, cfg.Version, &extraGroup))
	}

	return deploy.ErrorOrDone(errs.ErrorOrNil())
}

func (t *TiDBGroupV2) create(ctx context.Context, gates feature.Gates, cfg *api.DescribeClusterConfigOutput, extraGroup *cloudv1.ExtraGroupCluster) error {
	cli := t.EnvProvider.GetClient()
	comp := &extraGroup.Spec.TiDB

	dbgcfg, err := t.NewTiDBGroupConfig(ctx, gates, comp, cfg, extraGroup)
	if err != nil {
		return err
	}

	dbg := builder.NewTiDBGroupBuilder().Build(gates, dbgcfg)
	if err := k8s.Apply(ctx, cli, dbg); err != nil {
		return err
	}

	return nil
}

func (t *TiDBGroupV2) ClusterConfig() (*api.DescribeClusterConfigOutput, error) {
	conf, err := sdk.GetCurrentClusterConfiguration(t.Local, t.Cluster.Spec.ConfigRevisionRef)
	if err != nil {
		return nil, fmt.Errorf("failed to get current cluster configuration: %w", err)
	}
	clusterConfig, err := sdk.GenerateConfigOutput(t.Cluster, conf)
	if err != nil {
		return nil, fmt.Errorf("failed to generate cluster configuration: %w", err)
	}

	return clusterConfig, nil
}

func (t *TiDBGroupV2) delete(ctx context.Context, extraGroup *cloudv1.ExtraGroupCluster) error {
	remoteCli := t.EnvProvider.GetClient()

	// remove extragroup status
	// if extragroup tidb replicas is 0, it means the extragroup is deleted, we can just reset the status of extragroup,
	// and make full use of getDeletedObject and DeleteObject to ensure the deletion of TiDBCluster.
	// if the deletion failed, it can be noticed in the cluster event log by GetDeletedObject function error message.
	if isExtraGroupStatusExsisted(extraGroup) {
		// it only reset fields managed by TiDBCluster task, other fields such as lb status should be managed in extragroup.service task
		resetExtraGroupStatusForTiDBCluster(t.Cluster, extraGroup)
	}
	n := t.TidbGroupNamer(extraGroup.Spec).
		WithComponent(string(cloudv1.ComponentTiDB))
	dbg := &opv2.TiDBGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      n.GetTiDBName(),
			Namespace: n.GetTiDBNamespace(),
		},
	}

	if err := k8s.DeleteObjectCompleted(ctx, remoteCli, dbg); err != nil {
		return err
	}

	return nil
}

// TODO(liubo02): combine with ObserveExtraGroupClusterStatus in tidbgroup_status.go
func (t *TiDBGroupV2) observe(ctx context.Context, gates feature.Gates, version string, extraGroup *cloudv1.ExtraGroupCluster) error {
	n := t.TidbGroupNamer(extraGroup.Spec).
		WithComponent(string(cloudv1.ComponentTiDB))

	remoteCli := t.EnvProvider.GetClient()

	dbg := &opv2.TiDBGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name:      n.GetTiDBName(),
			Namespace: n.GetTiDBNamespace(),
		},
	}

	if err := remoteCli.Get(ctx, client.ObjectKeyFromObject(dbg), dbg); err != nil {
		return err
	}

	var dbl opv2.TiDBList
	if err := remoteCli.List(ctx, &dbl, client.InNamespace(n.GetTiDBNamespace()), client.MatchingLabels{
		opv2.LabelKeyCluster:   dbg.Spec.Cluster.Name,
		opv2.LabelKeyComponent: opv2.LabelValComponentTiDB,
		opv2.LabelKeyGroup:     dbg.Name,
	}); err != nil {
		return fmt.Errorf("failed to list tidb instances: %w", err)
	}
	dbs := make([]*opv2.TiDB, 0, len(dbl.Items))
	for i := range dbl.Items {
		dbs = append(dbs, &dbl.Items[i])
	}

	var nl corev1.NodeList
	if err := remoteCli.List(ctx, &nl, client.MatchingLabels(n.Labels())); err != nil {
		return fmt.Errorf("failed to list tidb nodes: %w", err)
	}

	var tidb *cloudv1.TiDBStatus
	if extraGroup.Status != nil {
		tidb = extraGroup.Status.TiDB
	}
	needSuspend := t.Cluster.Spec.Cluster.TiDB.NeedSuspend(t.Cluster.Spec.Cluster.SuspendAction)
	instanceType := extraGroup.Spec.TiDB.InstanceType
	tidb, err := status.NewTiDBResolver(
		t.Logger,
		gates,
		t.Provider,
		len(nl.Items),
		extraGroup.Spec.TiDB.GetReplicas(),
		needSuspend,
		instanceType,
		func(memberInstanceType string) bool {
			return t.Cluster.TiDBMemberInstanceTypeAllowedForGroup(memberInstanceType, extraGroup.Name, instanceType)
		},
	).ComputeTiDBStatus(tidb, dbg, dbs)
	if err != nil {
		return err
	}

	// secondaryStatus
	secondaryStatus := cloudv1.DBSecondaryStatusHealthy

	// PD partial failure cannot be felt by user, so the status is hidden
	if tidb.Status == cloudv1.ComponentUnhealthy {
		secondaryStatus = cloudv1.DBSecondaryStatusUnhealthy
	}

	// Phase
	var phase cloudv1.ClusterPhase
	switch tidb.Status {
	// cluster is considered as unavailable when one of TiKV, PD, TiDB, TiProxy is unavailable
	case cloudv1.ComponentUnavailable:
		if extraGroup.Status.Phase != cloudv1.ClusterPhaseSuspend {
			phase = cloudv1.ClusterPhaseUnavailable
			secondaryStatus = cloudv1.DBSecondaryStatusUnhealthy
		} else {
			phase = cloudv1.ClusterPhaseSuspend
		}
	// if both upgrading and scaling are ongoing (e.g. failover when upgrading), show Upgrading
	case cloudv1.ComponentUpgrading:
		phase = cloudv1.ClusterPhaseUpgrading
	case cloudv1.ComponentScaling:
		phase = cloudv1.ClusterPhaseScaling
	default:
		phase = cloudv1.ClusterPhaseRunning
	}

	// Resons
	reason := fmt.Sprintf("tidb: %s", tidb.Status)

	// Condition
	if extraGroup.Status.Conditions == nil {
		extraGroup.Status.Conditions = []cloudv1.ClusterCondition{}
	}
	cloudv1.SetStatusCondition(&extraGroup.Status.Conditions, t.buildSuspendCondition(tidb))

	if t.Cluster.Suspending() || t.Cluster.Suspended() {
		phase = cloudv1.ClusterPhaseSuspend
	}

	extraGroup.Status.Phase = phase
	extraGroup.Status.Reason = reason
	extraGroup.Status.TiDB = tidb
	extraGroup.Status.SecondaryStatus = secondaryStatus
	extraGroup.Status.Version = version

	if extraGroup.Status.Name == "" {
		extraGroup.Status.Name = extraGroup.Spec.Name
	}

	return nil
}

func (t *TiDBGroupV2) buildSuspendCondition(tidbStatus *cloudv1.TiDBStatus) cloudv1.ClusterCondition {
	suspendCondition := cloudv1.ClusterCondition{
		Type:    cloudv1.ClusterSuspended,
		Status:  cloudv1.ConditionTrue,
		Reason:  "ClusterSuspended",
		Message: "The cluster has been suspended",
	}
	var msg []string
	if tidbStatus == nil {
		msg = append(msg, "extragroup tidb status is nil")
	} else {
		if !meta.IsStatusConditionTrue(tidbStatus.Conditions,
			cloudv1.ComponentSuspended) {
			msg = append(msg, "TiDB has not been suspended")
		}
	}
	if len(msg) > 0 {
		suspendCondition = cloudv1.ClusterCondition{
			Type:    cloudv1.ClusterSuspended,
			Status:  cloudv1.ConditionFalse,
			Reason:  "ClusterNotSuspended",
			Message: strings.Join(msg, ";"),
		}
	}
	return suspendCondition
}

// getAuditSidecarConfig returns the audit sidecar image and envs.
// It prefers SidecarImageV2 if set, otherwise falls back to SidecarImage.
// If the cluster has an AuditLogSidecarImageTag annotation, that tag is applied on top,
// and if the sidecar version supports dynamic log path, envs are returned.
func (t *TiDBGroupV2) getAuditSidecarConfig(ctx context.Context, groupName string) (image string, sidecarEnvs []corev1.EnvVar) {
	return resolveAuditSidecarConfig(ctx, t.Cluster, t.Config, groupName)
}

func (t *TiDBGroupV2) NewTiDBGroupConfig(
	ctx context.Context,
	gates feature.Gates,
	comp cloudv1.ClusterComponent,
	cfg *api.DescribeClusterConfigOutput,
	extraGroup *cloudv1.ExtraGroupCluster,
) (*builder.TiDBGroupConfig, error) {
	n := t.TidbGroupNamer(extraGroup.Spec).
		WithComponent(string(cloudv1.ComponentTiDB))

	sidecarImage, sidecarEnvs := t.getAuditSidecarConfig(ctx, extraGroup.Name)
	dbgcfg := &builder.TiDBGroupConfig{
		Name:                n.GetTiDBName(),
		Namespace:           n.GetTiDBNamespace(),
		ClusterRef:          n.GetDefaultTiDBName(),
		ClusterName:         n.GetClusterName(),
		AuthTokenSecretName: n.TidbAuthTokenJwksSecret(),

		// directly from cluster component
		InstanceType: comp.GetInstanceType(),
		Replicas:     int32(comp.GetReplicas()),
		Arch:         getInstanceArch(t.Provider, comp.GetInstanceType()),

		HelperImage: config.GetOverrideBusyboxImage(t.Cluster.Annotations),

		// from context
		Zones:      t.Zones(),
		K8sVersion: t.GetNodeVersion(true),

		// from cluster
		TenantID:  t.Cluster.GetTenantID(),
		ProjectID: t.Cluster.GetProjectID(),
		ClusterID: t.Cluster.GetClusterID(),
		Provider:  t.Config.Provider,

		TiDBServiceScope: extraGroup.Name,

		AuditLogSidecarImage:   sidecarImage,
		AuditLogSidecarCommand: t.Config.ClusterController.Audit.SidecarCommand,
		AuditLogSidecarEnvs:    sidecarEnvs,
	}

	var compConfig *api.ComponentOutput
	for i, c := range cfg.Components {
		if c.Component == api.ComponentTiDB {
			compConfig = &cfg.Components[i]
			break
		}
	}

	// OriginalConfig
	dbgcfg.OriginalConfig = compConfig.Config.MP

	// Resources
	resourceSpec, err := t.ConfigProvider.GetResourceSpec(t.Provider, comp)
	if err != nil {
		return nil, fmt.Errorf("failed to get resources spec: %w", err)
	}
	dbgcfg.Resources = resourceSpec.Resources

	// StorageClassName
	sc, ok := comp.GetStorageClassName()
	if !ok {
		sc = resourceSpec.StorageClass
	}
	sc = t.CMEKStorageClassMapper.Name(sc)
	dbgcfg.StorageClassName = sc

	// StorageSize
	size, ok := comp.GetStorageSize()
	if ok {
		dbgcfg.StorageSize = size
	}

	// BaseImage and ImageTag
	dbgcfg.BaseImage = compConfig.Image.BaseImage
	dbgcfg.ImageTag = cfg.Version
	if compConfig.Image.Tag != nil {
		dbgcfg.ImageTag = *compConfig.Image.Tag
	}

	return dbgcfg, nil
}
