package status

import (
	"fmt"
	"sort"

	"github.com/go-logr/logr"
	opv2 "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task/common/feature"
	"github.com/tidbcloud/eks-provider/utils/tidb-operator/v2utils"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/gitops"
)

type TiDBResolver interface {
	ComputeTiDBStatus(status *cloudv1.TiDBStatus, dbg *opv2.TiDBGroup, dbs []*opv2.TiDB) (*cloudv1.TiDBStatus, error)
}

type dbgResolver struct {
	p     gitops.Provider
	gates feature.Gates

	// Now it only used to check whether all nodes are deleted.
	// It introduces an api call to fetch all nodes to update status.
	// Maybe we can avoid it to make status updation simpler
	// TODO(liubo02): avoid fetching nodes
	nodeCount int

	desiredReplicas int

	// It means cluster should suspend
	needSuspend bool

	// expected instance type
	instanceType string

	// isInstanceTypeAllowed lets heterogeneous TiDB groups report non-base
	// members without marking an instance-type scale as still in progress.
	isInstanceTypeAllowed func(string) bool

	log logr.Logger
}

func NewTiDBResolver(
	log logr.Logger,
	gates feature.Gates,
	p gitops.Provider,
	nodeCount, desiredReplicas int,
	needSuspend bool,
	instanceType string,
	isInstanceTypeAllowed func(string) bool,
) TiDBResolver {
	return &dbgResolver{
		p:     p,
		gates: gates,

		nodeCount:             nodeCount,
		desiredReplicas:       desiredReplicas,
		needSuspend:           needSuspend,
		instanceType:          instanceType,
		isInstanceTypeAllowed: isInstanceTypeAllowed,
		log:                   log,
	}
}

func (r *dbgResolver) ComputeTiDBStatus(status *cloudv1.TiDBStatus, dbg *opv2.TiDBGroup, dbs []*opv2.TiDB) (*cloudv1.TiDBStatus, error) {
	expectedInstanceType := r.instanceType
	instanceTypeChanged := false

	members, err := r.computeMembers(dbs)
	if err != nil {
		return nil, err
	}
	for _, m := range members {
		if m.InstanceType == "" || m.InstanceType == expectedInstanceType {
			continue
		}
		if r.isInstanceTypeAllowed == nil || !r.isInstanceTypeAllowed(m.InstanceType) {
			instanceTypeChanged = true
		}
	}

	if status == nil {
		status = &cloudv1.TiDBStatus{}
	}

	instanceTypeChanged = status.InstanceTypeScalePhase != cloudv1.InstanceTypeScalePhaseNone || instanceTypeChanged

	status.Members = members
	status.Status = r.computeComponentStatus(dbg, instanceTypeChanged)
	status.UpdatedReplicas = dbg.Status.UpdatedReplicas
	status.TokenBasedAuthEnabled = r.gates.Enabled(feature.TokenBasedAuth)
	status.Conditions = computeComponentConditions(status.Conditions, dbg.Status.Conditions, r.nodeCount, r.needSuspend)

	return status, nil
}

func (r *dbgResolver) computeMembers(dbs []*opv2.TiDB) ([]cloudv1.TiDBMember, error) {
	members := make([]cloudv1.TiDBMember, 0, len(dbs))
	for _, tidb := range dbs {
		instanceType, ok := tidb.Annotations[consts.TcInstanceTypeAnnotationKey]
		if !ok {
			return nil, fmt.Errorf("missing instance type annotation of the tidb %s/%s", tidb.Namespace, tidb.Name)
		}
		it, err := r.p.FindInstanceType(instanceType)
		if err != nil {
			return nil, fmt.Errorf("cannot get info of instance type(%s)", instanceType)
		}
		zone, ok := tidb.Spec.Topology[consts.ZoneLabelKey]
		if !ok {
			return nil, fmt.Errorf("missing zone of the tidb %s/%s", tidb.Namespace, tidb.Name)
		}

		members = append(members, cloudv1.TiDBMember{
			Name:   InstanceNameToPodName(tidb.Name, "tidb"),
			Labels: map[string]string{cloudv1.MemberLabelAvailableZone: zone},
			Resources: cloudv1.Resources{
				CPU:    it.CPU,
				Memory: it.Memory,
			},
			Status:       computeMemberStatus(tidb.Status.Conditions, !tidb.DeletionTimestamp.IsZero()),
			InstanceType: instanceType,
		})
	}
	sort.Slice(members, func(i, j int) bool {
		return members[i].Name < members[j].Name
	})

	return members, nil
}

func (r *dbgResolver) computeComponentStatus(dbg *opv2.TiDBGroup, instanceTypeChanged bool) cloudv1.ComponentStatus {
	r.log.Info("Compute tidb component status", "readyReplicas", dbg.Status.ReadyReplicas,
		"instanceTypeChanged", instanceTypeChanged, "actualReplicas", dbg.Status.Replicas, "desiredReplicas", r.desiredReplicas)
	var cs cloudv1.ComponentStatus
	switch {
	case dbg.Status.ReadyReplicas == 0:
		cs = cloudv1.ComponentUnavailable
	case int(dbg.Status.Replicas) != r.desiredReplicas || instanceTypeChanged:
		// when replicas is changed or instance type is changed
		cs = cloudv1.ComponentScaling
	case !v2utils.IsSynced(dbg.Status.Conditions):
		cs = cloudv1.ComponentUpgrading
	case !v2utils.IsReady(dbg.Status.Conditions):
		cs = cloudv1.ComponentUnhealthy
	default:
		cs = cloudv1.ComponentNormal
	}

	return cs
}
