package v1

import (
	"fmt"

	"golang.org/x/exp/slices"
)

type ExtraGroupCluster struct {
	Name   string
	Spec   *ExtraGroupSpec
	Status *ExtraGroupStatus
}

func (c *Cluster) GetExtraGroups() []ExtraGroupCluster {
	extraGroupClusters := make([]ExtraGroupCluster, len(c.Spec.ExtraGroups))
	extraEndpointStatusMap := make(map[string]*ExtraGroupStatus, len(c.Status.ExtraEndpoints))

	for i := range c.Status.ExtraEndpoints {
		extraEndpointStatusMap[c.Status.ExtraEndpoints[i].Name] = &c.Status.ExtraEndpoints[i]
	}

	for i := range c.Spec.ExtraGroups {
		if extraEndpointStatusMap[c.Spec.ExtraGroups[i].Name] == nil {
			initExtraGroupStatus := ExtraGroupStatus{
				Name: c.Spec.ExtraGroups[i].Name,
			}
			c.Status.ExtraEndpoints = append(c.Status.ExtraEndpoints, initExtraGroupStatus)
			extraEndpointStatusMap[c.Spec.ExtraGroups[i].Name] = &initExtraGroupStatus
		}
		extraGroupClusters[i] = ExtraGroupCluster{
			Name:   c.Spec.ExtraGroups[i].Name,
			Spec:   &c.Spec.ExtraGroups[i],
			Status: extraEndpointStatusMap[c.Spec.ExtraGroups[i].Name],
		}
	}

	return extraGroupClusters
}

func (c *Cluster) GetExtraGroupSpec(groupId string) (*ExtraGroupSpec, bool) {
	for i := range c.Spec.ExtraGroups {
		if c.Spec.ExtraGroups[i].Name == groupId {
			return &c.Spec.ExtraGroups[i], true
		}
	}

	return nil, false
}

func (c *Cluster) GetExtraGroupStatus(groupId string) (*ExtraGroupStatus, bool) {
	for i := range c.Status.ExtraEndpoints {
		if c.Status.ExtraEndpoints[i].Name == groupId {
			return &c.Status.ExtraEndpoints[i], true
		}
	}
	return nil, false
}

func (c *Cluster) GetExtraGroup(groupId string) (*ExtraGroupCluster, bool) {
	for _, g := range c.GetExtraGroups() {
		if g.Name == groupId {
			return &g, true
		}
	}

	return nil, false
}

// ExtraGroupSync make sure the status is synced as the spec.
// we can use it to make sure the update is applied when we update the replica, strorage or instance in the spec.
func (c *Cluster) ExtraGroupSync() bool {
	if c.Status.Phase != ClusterPhaseRunning {
		return false
	}

	// All extragroup should synced
	for _, extraGroup := range c.GetExtraGroups() {
		if !c.ExtraGroupSynced(&extraGroup) {
			return false
		}
	}

	return true
}

// ExtraGroupGetUpToDateReplicas returns the replicas count which has been up to date.
// some special cases:
// - scale up: the member in TiDB Cluster becomes disconnected delay and the new pod may already started in the new instance
// - scale out + scale up: scaled out replicas with new instance-type and become normal before rolling-update started
// A member can only be marked as up to date until the member becomes normal again in the new instance,
// but we can't handle this accurately and only ensure:
// - the progress never changed back
// - all members become normal when progress reached 100%
func (c *Cluster) ExtraGroupGetUpToDateReplicas(comp ComponentType) int {
	extraGroups := c.GetExtraGroups()

	totalReplicas := 0
	for _, extraGroup := range extraGroups {
		itUpdatedReplicas := 0
		replicas := 0
		if extraGroup.Spec.TiDB.Replicas == 0 {
			// extragroup is just created, not really used, so we can ignore it
			continue
		}
		if extraGroup.Status.TiDB == nil {
			continue // "0" if no status exists yet
		}

		dbStatus := extraGroup.Status

		switch comp {
		case ComponentTiDB:
			tidbInstanceTypePolicy := c.extraGroupTiDBMemberInstanceTypePolicy(extraGroup)
			for _, member := range dbStatus.TiDB.Members {
				if tidbInstanceTypePolicy.allows(member.InstanceType) &&
					(member.Status == MemberStatusNormal || member.Status == MemberStatusUnavailable) {
					itUpdatedReplicas++
				}
			}
			if itUpdatedReplicas == 0 {
				replicas = 0
				break
			}

			replicas = int(dbStatus.TiDB.UpdatedReplicas)
			if itUpdatedReplicas < replicas {
				replicas = itUpdatedReplicas
			}
			// NOTE: After discuss with @csuzhangxc, one guarantee of this function annotated in function comment
			// "all members become normal when progress reached 100%" that is UNNECESSARY. All components modification of cluster
			// have been done don't mean the cluster modification is done. So we don't need to do this hack way to ensure it.
			// if replicas == c.Spec.Cluster.TiDB.Replicas && db.TiDB.Status != ComponentNormal {
			//	if c.Spec.Cluster.TiDB.Replicas >= len(db.TiDB.Members) {
			//		replicas = replicas - 1
			//	}
			// }
		}

		totalReplicas += replicas
	}

	return totalReplicas
}

func (c *Cluster) NormalizeExtraGroupStatus() {
	ees := []ExtraGroupStatus{}
	for i := range c.Spec.ExtraGroups {
		group := &c.Spec.ExtraGroups[i]
		found := false
		for k := range c.Status.ExtraEndpoints {
			status := &c.Status.ExtraEndpoints[k]
			if group.Name == status.Name {
				ees = append(ees, *status)
				found = true
				break
			}
		}

		if !found {
			ees = append(ees, ExtraGroupStatus{
				Name: group.Name,
			})
		}
	}

	c.Status.ExtraEndpoints = ees
}

func (c *ExtraGroupCluster) Deleted() bool {
	return c.Spec.Deleted()
}

func (c *ExtraGroupCluster) Synced() bool {
	return (*Cluster)(nil).ExtraGroupSynced(c)
}

func (c *ExtraGroupCluster) TiDBSynced() bool {
	if c == nil || c.Spec == nil || c.Status == nil {
		return false
	}
	return (*Cluster)(nil).extraGroupTiDBSynced(*c)
}

func (c *ExtraGroupCluster) TiProxySynced() bool {
	if c == nil || c.Spec == nil || c.Status == nil {
		return false
	}
	return (*Cluster)(nil).extraGroupTiProxySynced(*c)
}

func (c *Cluster) ExtraGroupSynced(extraGroup *ExtraGroupCluster) bool {
	if extraGroup == nil || extraGroup.Spec == nil || extraGroup.Status == nil {
		return false
	}
	return c.extraGroupTiDBSynced(*extraGroup) && c.extraGroupTiProxySynced(*extraGroup)
}

func (c *Cluster) extraGroupTiDBSynced(extraGroup ExtraGroupCluster) bool {
	spec := extraGroup.Spec.TiDB
	status := extraGroup.Status.TiDB
	if spec.Replicas == 0 {
		if status != nil && len(status.Members) != 0 {
			return false
		}
	} else {
		if status == nil || status.Status != ComponentNormal || len(status.Members) != spec.Replicas {
			return false
		}
		tidbInstanceTypePolicy := c.extraGroupTiDBMemberInstanceTypePolicy(extraGroup)
		for _, member := range status.Members {
			if !tidbInstanceTypePolicy.allows(member.InstanceType) {
				// NOTE: we treat `member.InstanceType == ""` as Synced as some providers not implment instance type scale.
				// check `InstanceType` rather than `InstanceTypeScalePhase` so that we can report Synced correctly before provider reconcile.
				return false
			}
		}
		if status.InstanceTypeScalePhase != InstanceTypeScalePhaseNone {
			return false
		}
	}

	return true
}

func (c *Cluster) extraGroupTiDBMemberInstanceTypePolicy(extraGroup ExtraGroupCluster) memberInstanceTypePolicy {
	spec := extraGroup.Spec.TiDB
	if c == nil {
		return memberInstanceTypePolicy{
			baseInstanceType: spec.InstanceType,
		}
	}
	return c.tiDBMemberInstanceTypePolicy(extraGroup.Name, spec.InstanceType)
}

func (c *Cluster) extraGroupTiProxySynced(extraGroup ExtraGroupCluster) bool {
	spec := extraGroup.Spec.TiProxy
	status := extraGroup.Status.TiProxy
	if spec.Replicas == 0 {
		if status != nil && len(status.Members) != 0 {
			return false
		}
	} else {
		if status == nil || status.Status != ComponentNormal || len(status.Members) != spec.Replicas {
			return false
		}
		for _, member := range status.Members {
			if member.InstanceType != "" && member.InstanceType != spec.InstanceType {
				return false
			}
		}
		if status.InstanceTypeScalePhase != InstanceTypeScalePhaseNone {
			return false
		}
	}

	return true
}

func (c *Cluster) GetAllReplicasByComp(comp ComponentType) (cnt int, err error) {
	if !slices.Contains(c.GetAllComponentTypes(), comp) {
		return cnt, fmt.Errorf("invalid cluster's component type: %s", comp)
	}

	// Get the component replicas in the default group
	cnt = c.GetComponent(comp).GetReplicas()

	// Get the component replicas across all extra groups
	extraGroups := c.GetExtraGroups()
	// Component can be set in extra groups and there are extraGroups
	if c.IsValidGroupComponent(comp) && len(extraGroups) > 0 {
		for _, group := range extraGroups {
			groupComp := group.Spec.GetComponent(comp)
			cnt += groupComp.GetReplicas()
		}
	}

	return
}

func (c *Cluster) IsValidGroupComponent(comp ComponentType) bool {
	return comp == ComponentTiDB || comp == ComponentTiProxy
}

func (c *ExtraGroupSpec) GetComponent(comp ComponentType) ClusterComponent {
	switch comp {
	case ComponentTiProxy:
		return &c.TiProxy
	case ComponentTiDB:
		return &c.TiDB
	default:
		panic(fmt.Sprintf("invalid component type(%s) for tidbgroup(%s)", comp, c.Name))
	}
}

func (c *ExtraGroupCluster) GetComponentStatus(comp ComponentType) ClusterComponentStatus {
	if c.Status == nil {
		panic("ExtraGroupCluster status is nil")
	}

	switch comp {
	case ComponentTiProxy:
		return c.Status.TiProxy
	case ComponentTiDB:
		return c.Status.TiDB
	default:
		panic(fmt.Sprintf("invalid component type for group(%s): %s", c.Name, comp))
	}
}

func (c *ExtraGroupCluster) GetOrCreateComponentStatus(comp ComponentType) ClusterComponentStatus {
	if c.Status == nil {
		panic("ExtraGroupCluster status is nil")
	}

	switch comp {
	case ComponentTiProxy:
		if c.Status.TiProxy == nil {
			c.Status.TiProxy = &TiProxyStatus{}
		}
	case ComponentTiDB:
		if c.Status.TiDB == nil {
			c.Status.TiDB = &TiDBStatus{}
		}
	}

	return c.GetComponentStatus(comp)
}

func (c *ExtraGroupSpec) GetComponents() []ClusterComponent {
	components := []ClusterComponent{
		&c.TiDB,
	}
	if c.TiProxy.Replicas > 0 {
		components = append(components, &c.TiProxy)
	}
	return components
}
