package cdc

import (
	"context"
	"fmt"

	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	corev1 "k8s.io/api/core/v1"
	storagev1 "k8s.io/api/storage/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/controllers/common"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/nodegroup"
	xpnodegroup "github.com/tidbcloud/infra-provider/pkg/nodegroup"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/pkg/util/subnet"
)

type AwsCdc struct {
	Cdc
}

func NewAwsCdc(base *Cdc) *AwsCdc {
	return &AwsCdc{
		Cdc: *base,
	}
}

func (p *AwsCdc) CreateCdcNodeGroup(ctx context.Context, ticdc *cloudv1.TiCDC, cls *cloudv1.Cluster, ngSet *nodegroup.Set) (map[string]string, error) {
	network, err := p.getCdcNetwork(ctx, ticdc)
	if err != nil {
		return nil, fmt.Errorf("failed find cdc located network: %v", err)
	}
	if len(ngSet.NodeGroups) == 0 {
		return nil, fmt.Errorf("no node group found")
	}
	networkSpec, err := v1alpha1.EKSNetworkSpecForNetwork(network)
	if err != nil {
		return nil, fmt.Errorf("unable to get network spec: %v", err)
	}

	ng := ngSet.NodeGroups[0]
	ng.Name = tidbNodeGroupName(ngSet.Name, string(ticdc.ObjectMeta.GetUID()))
	ngNamer := namer.NewNamer().LegacyWithNetworkName(network.Name).WithComponent(string(ng.ComponentType))
	ng.Labels = util.ConcatenateTags(ng.Labels, cloudv1.TagsFromLabels(network.Labels))
	ng.Labels = util.ConcatenateTags(ng.Labels, map[string]string{
		// override the network "usedby":"dbaas-control-plane-shoot" label
		common.UsedBy:      common.CustomerTiDB,
		common.Component:   string(cloudv1.ComponentTiCDC),
		common.Cluster:     ticdc.Spec.ClusterName,
		common.ServiceType: common.Dedicated,
		common.Environment: p.Config.GetEnvironment(),
	})
	ng.Labels = util.ConcatenateTags(ng.Labels, ngNamer.Labels())
	ng.MachineSpec.SystemVolumeSize = defaultSystemVolumeSize

	// concat servicetype tag
	subnets, err := subnet.DescribeSubnets(p.AWSSVC, network)
	if err != nil {
		return nil, err
	}

	group, ngLaunchTemplate, ngMut, launchTemplateMut := xpnodegroup.FromNodegroupSpec(ng, network, networkSpec, subnet.FindSubnetIdsByZones(subnets, ng.Zones), nil, nil /*capacityType*/, true)

	if _, err := controllerutil.CreateOrUpdate(ctx, p.Cli, ngLaunchTemplate, launchTemplateMut); err != nil {
		return nil, err
	}

	if len(ngLaunchTemplate.Status.Conditions) == 0 {
		return nil, fmt.Errorf("launch template condition is empty, wait for next check")
	}
	for _, condition := range ngLaunchTemplate.Status.Conditions {
		if condition.Status != corev1.ConditionTrue {
			log.FromContext(ctx).Info(
				"launch template is not ready", "type", condition.Type, "status", condition.Status, "reason", condition.Reason)
			return nil, fmt.Errorf("launch template type %s is not ready", condition.Type)
		}
	}

	result, err := controllerutil.CreateOrUpdate(ctx, p.Cli, group, ngMut)
	if err != nil {
		return nil, err
	}
	if result == controllerutil.OperationResultCreated {
		log.FromContext(ctx).Info(
			"create aws node group for ticdc successfully", "ticdc", ticdc.Name, "node group", group)
	} else if result == controllerutil.OperationResultUpdated {
		log.FromContext(ctx).Info(
			"update aws node group for ticdc successfully", "ticdc", ticdc.Name, "node group", group)
	}
	return ng.Labels, nil
}

func (p *AwsCdc) DeleteCdcNodeGroup(ctx context.Context, ticdc *cloudv1.TiCDC, ngSet *nodegroup.Set) error {
	network, err := p.getCdcNetwork(ctx, ticdc)
	ngName := namer.UniqueIdentifier(network.ObjectMeta, namer.WithPrefix(tidbNodeGroupName(ngSet.Name, string(ticdc.ObjectMeta.GetUID()))))
	if err != nil {
		return fmt.Errorf("failed find cdc located network: %v", err)
	}
	group := &manualv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: ngName,
		},
	}
	if err := p.Cli.Delete(ctx, group, &client.DeleteOptions{}); err != nil {
		return err
	}

	ngLaunchTemplate := &ec2v1alpha1.LaunchTemplate{
		ObjectMeta: metav1.ObjectMeta{
			Name: ngName,
		},
	}
	return p.Cli.Delete(ctx, ngLaunchTemplate, &client.DeleteOptions{})
}

func (p *AwsCdc) ReleaseNodes(ctx context.Context, cr *cloudv1.TiCDC, ngNamePrefix string, labelSet labels.Set) error {
	set := &nodegroup.Set{
		Name: ngNamePrefix,
	}
	if err := p.DeleteCdcNodeGroup(ctx, cr, set); client.IgnoreNotFound(err) != nil {
		return err
	}

	// protect delete label set not specify ticdc.cr.id
	if _, exist := labelSet[TiCDCCrIdLabel]; !exist {
		labelSet[TiCDCCrIdLabel] = string(cr.UID)
	}
	// delete all nodegroup of target ticdc cluster
	if err := p.Cli.DeleteAllOf(ctx, &manualv1alpha1.NodeGroup{}, client.MatchingLabelsSelector{
		Selector: labelSet.AsSelector(),
	}); client.IgnoreNotFound(err) != nil {
		return err
	}
	if err := p.Cli.DeleteAllOf(ctx, &ec2v1alpha1.LaunchTemplate{}, client.MatchingLabelsSelector{
		Selector: labelSet.AsSelector(),
	}); client.IgnoreNotFound(err) != nil {
		return err
	}

	return nil
}

func (p *AwsCdc) UpdateNodegroupTopology(ctx context.Context, cr *cloudv1.TiCDC, topology map[string]int) error {

	var list manualv1alpha1.NodeGroupList
	if err := p.Cli.List(ctx, &list, client.MatchingLabels{
		TiCDCCrIdLabel:    string(cr.UID),
		TiCDCClusterLabel: *cr.Spec.CDCClusterID,
	}); client.IgnoreNotFound(err) != nil {
		return err
	}
	// ensure current exist one group ng
	if len(list.Items) != DefaultTiCDCZoneNum {
		return fmt.Errorf("current ng num %d is not equal to default ticdc zone %d", len(list.Items), DefaultTiCDCZoneNum)
	}
	for _, ng := range list.Items {
		az, exist := ng.Labels[TiCDCZoneLabel]
		if !exist {
			return fmt.Errorf("ng %s has a empty zone label", ng.Name)
		}
		num, exist := topology[az]
		if !exist {
			num = 0
		}
		ng.Spec.ForProvider.ScalingConfig.MinSize = pointer.Int32(int32(num))
		ng.Spec.ForProvider.ScalingConfig.MaxSize = pointer.Int32(int32(num))
		ng.Spec.ForProvider.ScalingConfig.DesiredSize = pointer.Int32(int32(num))
		if num == 0 {
			// Minimum value of MaxSize 1
			// https://docs.aws.amazon.com/zh_cn/eks/latest/APIReference/API_NodegroupScalingConfig.html
			ng.Spec.ForProvider.ScalingConfig.MaxSize = pointer.Int32(1)
		}
		if err := p.Cli.Update(ctx, &ng); err != nil {
			return err
		}
	}

	return nil
}

func (p *AwsCdc) SetResourceLabels(ctx context.Context, cr *cloudv1.TiCDC) error {
	return nil
}

func (p *AwsCdc) EnsureAllNodegroupsReady(ctx context.Context, cr *cloudv1.TiCDC) error {
	var list manualv1alpha1.NodeGroupList
	if err := p.Cli.List(ctx, &list, client.MatchingLabels{
		TiCDCCrIdLabel:    string(cr.UID),
		TiCDCClusterLabel: *cr.Spec.CDCClusterID,
	}); client.IgnoreNotFound(err) != nil {
		return err
	}
	for _, ng := range list.Items {
		if ng.Status.AtProvider.Status != manualv1alpha1.NodeGroupStatusActive {
			return fmt.Errorf("wait nodegroup %s status %s, current status %s", ng.Name, manualv1alpha1.NodeGroupStatusActive, ng.Status.AtProvider.Status)
		}
	}

	return nil
}

func (p *AwsCdc) DeleteNodegroupsByFilter(ctx context.Context, cr *cloudv1.TiCDC, selector labels.Selector) error {
	var ngList manualv1alpha1.NodeGroupList
	err := p.Cli.List(ctx, &ngList, &client.MatchingLabelsSelector{Selector: selector})
	if err != nil {
		return err
	}
	for _, ng := range ngList.Items {
		if ng.Labels[TiCDCCrIdLabel] != string(cr.UID) {
			continue
		}
		group := &manualv1alpha1.NodeGroup{
			ObjectMeta: metav1.ObjectMeta{
				Name: ng.Name,
			},
		}
		if err := p.Cli.Delete(ctx, group, &client.DeleteOptions{}); err != nil {
			return err
		}

		ngLaunchTemplate := &ec2v1alpha1.LaunchTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name: ng.Name,
			},
		}
		if err := p.Cli.Delete(ctx, ngLaunchTemplate, &client.DeleteOptions{}); err != nil {
			return err
		}
	}

	return nil
}

func (p *AwsCdc) GetNodegroupsByFilter(ctx context.Context, cr *cloudv1.TiCDC, selector labels.Selector) (map[string]int, error) {
	topology := make(map[string]int)
	var ngList manualv1alpha1.NodeGroupList
	err := p.Cli.List(ctx, &ngList, &client.MatchingLabelsSelector{Selector: selector})
	if err != nil {
		return nil, err
	}
	for _, ng := range ngList.Items {
		zone, exist := ng.Labels[TiCDCZoneLabel]
		if !exist {
			return nil, fmt.Errorf("current node group %s dose not have zone lable", ng.Name)
		}
		// max size may be 1, desired size 0, min size is always the same as actual node number
		num := *ng.Spec.ForProvider.ScalingConfig.MinSize
		topology[zone] = int(num)
	}
	return topology, nil
}

func (p *AwsCdc) Credentials(ctx context.Context, network *cloudv1.Network) ([]byte, error) {
	// TODO: add aws credentials here
	return nil, nil
}

func (p *AwsCdc) IsDPCompatMode() bool {
	return config.FeatureEnabled(p.Config.FeatureGates, config.EnableDPCompatibilityMode, "", "")
}

func (p *AwsCdc) BuildStorageClass() *storagev1.StorageClass {
	sc := cloudv1.StorageClass{
		Name:       consts.TiCDCGp3StorageClassName,
		Type:       consts.AWSVolumeTypeGP3,
		IOPS:       consts.TiCDCGp3IOPS,
		Throughput: consts.TiCDCGp3ThroughputMBps,
	}
	return controllers.ConvertAWSStorageClass(sc)
}
