package network

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"reflect"
	"strconv"
	"strings"

	infrav1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/compat/common"
	compatcst "github.com/tidbcloud/infra-provider/pkg/compat/consts"
	models "github.com/tidbcloud/infra-provider/pkg/compat/models"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/tiinf/transport/crossregion"
	apperror "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
	errInvalidNetworkNameNs = "invalid network name or namespace name"
	errNoneNetworkType      = "none network type"
	AWSRoleTmpl             = "arn:aws:iam::%s:role/%s"
	networkurl              = "/inner/mgmt/compat/v1/networks/%s"
)

func getSubIds(subnets []models.Globaldedicatedv1CompatNetworkAwsNodeSubnet) []string {
	var subIds []string
	for _, subnet := range subnets {
		subIds = append(subIds, subnet.SubnetId)
	}
	return subIds
}

type CompatNetworkClienter interface {
	GetNetwork(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error
	GetPrivateSubnet(ctx context.Context, network *cloudv1.Network) ([]models.Globaldedicatedv1CompatNetworkAwsNodeSubnet, error)
}

type CompatNetworkClient struct {
	Env        crossregion.EnvType
	RoleName   string
	ExternalId string
}

func NewCompatNetworkClient(env crossregion.EnvType, roleName string, dataPlaneRoleArn string, assumeExternalId string) *CompatNetworkClient {
	return &CompatNetworkClient{
		Env:        env,
		RoleName:   roleName,
		ExternalId: assumeExternalId,
	}
}

func (cnc CompatNetworkClient) GetNetwork(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
	logger := log.FromContext(ctx).WithName("CompatNetworkClient")

	network, ok := obj.(*cloudv1.Network)
	if !ok {
		return apperror.NewInternalError(errors.New(errNoneNetworkType))
	}
	//name link eks-us-west-2, we should get provider type and region from it
	providerType, region, ok := strings.Cut(key.Name, "-")
	if !ok {
		logger.Info("key name is invalid", "key", key.Name, "namespace", key.Namespace)
		return apperror.NewInternalError(errors.New(errInvalidNetworkNameNs))
	}
	logger.Info("get network", "providerType", providerType, "region", region)
	//ns like nightly-ms-12345 or prod-12345
	//we should get project id from ns last part like 12345
	namespaces := strings.Split(key.Namespace, "-")
	if len(namespaces) < 2 {
		return apperror.NewInternalError(errors.New(errInvalidNetworkNameNs))
	}
	projectstr := namespaces[len(namespaces)-1]
	logger.Info("get network", "project", projectstr)
	querydata := map[string]string{
		"providerName": providerType,
		"regionName":   region,
	}
	resp, code, err := common.GetGlobalHTTP[models.Globaldedicatedv1CompatNetwork](ctx, cnc.Env, networkurl, projectstr, &querydata)
	if err != nil {
		if code == int(bizerr.ErrCodeRecordNotFound) {
			return apperror.NewNotFound(schema.GroupResource{
				Group:    obj.GetObjectKind().GroupVersionKind().Group,
				Resource: obj.GetObjectKind().GroupVersionKind().Kind,
			}, key.Name)
		}
		return apperror.NewInternalError(err)
	}
	if code != http.StatusOK {
		if code == http.StatusNotFound && resp == nil {
			return apperror.NewNotFound(schema.GroupResource{
				Group:    obj.GetObjectKind().GroupVersionKind().Group,
				Resource: obj.GetObjectKind().GroupVersionKind().Kind,
			}, key.Name)
		}
		logger.Info("call global network failed", "code", code)
		return apperror.NewInternalError(fmt.Errorf("call global network failed, code=%d", code))
	}
	network.Name = key.Name
	network.Namespace = key.Namespace
	network.Labels = map[string]string{
		cloudv1.RegionLabel:       region,
		cloudv1.ProjectLabel:      projectstr,
		cloudv1.ProjectTagKey:     projectstr,
		cloudv1.TenantTagKey:      resp.TenantId,
		infrav1.ManageLabelKey:    infrav1.ManageLabelValueFull,
		cloudv1.ProviderTypeLabel: providerType,
	}
	network.Annotations = map[string]string{
		consts.AnnotationKeyDataplaneName:     resp.K8sName,
		compatcst.CompatK8SCAAnnotation:       resp.K8sCa,
		compatcst.CompatK8SEndpointAnnotation: resp.K8sEndpoint,
		compatcst.CompatK8SReadyAnnotation:    strconv.FormatBool(resp.K8sReady),
		compatcst.CompatFlagAnnotation:        strconv.FormatBool(true),
	}
	network.Spec = cloudv1.NetworkSpec{
		Provider: providerType,
		Region:   region,
	}

	if providerType == consts.ProviderTypeEKS {
		assumeRoleARN := fmt.Sprintf(AWSRoleTmpl, resp.Iam.AccountId, cnc.RoleName)
		network.Annotations[compatcst.CompatAWSDataPlaneAssumeRoleArn] = assumeRoleARN
		network.Annotations[compatcst.CompatAWSDataPlaneAssumeExternalId] = cnc.ExternalId

		network.Status = cloudv1.NetworkStatus{
			IAM: cloudv1.IAMStatus{
				AccountID:  resp.Iam.AccountId,
				ExternalId: resp.Iam.ExternalId,
			},
		}
		awsinfo := resp.Aws
		nodeSubnets := make([]models.Globaldedicatedv1CompatNetworkAwsNodeSubnet,
			0, len(awsinfo.NodeSubnets)+len(awsinfo.SecondaryNodeSubnets))
		nodeSubnets = append(nodeSubnets, awsinfo.NodeSubnets...)
		nodeSubnets = append(nodeSubnets, awsinfo.SecondaryNodeSubnets...)
		subIds := getSubIds(nodeSubnets)
		eksSpec := &infrav1.EKSNetworkSpec{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "eks.infra.pingcap.com/v1alpha1",
				Kind:       reflect.TypeOf(infrav1.EKSNetworkSpec{}).Name(),
			},
			VpcId:             resp.VpcId,
			SecurityGroupId:   awsinfo.SecurityGroupId,
			SubnetIds:         subIds,
			NodeRoleArn:       resp.NodeRole,
			K8sClusterVersion: resp.K8sVersion,
			AssumeRoleArn:     assumeRoleARN,  // we should build role arn from account id
			AssumeExternalId:  cnc.ExternalId, //we should get external id from config
		}

		err = infrav1.SetEKSNetworkSpec(network, eksSpec)
		if err != nil {
			return apperror.NewInternalError(err)
		}
		eksStatus := &infrav1.EKSNetworkStatus{
			CommonNetworkStatus: infrav1.CommonNetworkStatus{
				ProxyBaseUrl: resp.ProxyBaseUrl,
			},
		}
		err = infrav1.SetEKSNetworkStatus(network, eksStatus)
		if err != nil {
			return apperror.NewInternalError(err)
		}
	} else if providerType == consts.ProviderTypeGKE {
		network.Status = cloudv1.NetworkStatus{
			IAM: cloudv1.IAMStatus{
				ServiceAccountEmail: resp.NodeRole, // in gke, node role is sa email
			},
		}
		gcpinfo := resp.Gcp
		gkeSpec := &infrav1.GKENetworkSpec{
			TypeMeta: metav1.TypeMeta{
				APIVersion: "cloud.pingcap.com/v1",
				Kind:       "Unknown",
			},
			ProjectID:         resp.Iam.AccountId, // accountid is projectid when gke
			Network:           resp.VpcId,         // vpcid is network name when gke
			Subnets:           gcpinfo.NodeSubnets,
			K8sClusterVersion: resp.K8sVersion,
		}
		//set gke sa to annotation, we can use it to create noodepool in new path
		network.Annotations[compatcst.CompatGKESAAnnotation] = resp.NodeRole
		err = infrav1.SetGKENetworkSpec(network, gkeSpec)
		if err != nil {
			return apperror.NewInternalError(err)
		}
		gkeStatus := &infrav1.GKENetworkStatus{
			CommonNetworkStatus: infrav1.CommonNetworkStatus{
				ProxyBaseUrl: resp.ProxyBaseUrl,
			},
		}
		err = infrav1.SetGKENetworkStatus(network, gkeStatus)
		if err != nil {
			return apperror.NewInternalError(err)
		}
	}
	//set network status phase
	if resp.K8sReady {
		network.Status.Phase = cloudv1.NetworkPhaseNormal
	} else {
		network.Status.Phase = cloudv1.NetworkPhaseCreating
	}
	logger.Info("get network", "network", network)
	return nil
}

func (cnc CompatNetworkClient) GetPrivateSubnet(ctx context.Context, network *cloudv1.Network) ([]models.Globaldedicatedv1CompatNetworkAwsNodeSubnet, error) {
	logger := log.FromContext(ctx).WithName("CompatNetworkClient")
	//name link eks-us-west-2, we should get provider type and region from it
	providerType, region, ok := strings.Cut(network.Name, "-")
	if !ok {
		logger.Info("network name is invalid", "key", network.Name, "namespace", network.Namespace)
		return nil, apperror.NewInternalError(errors.New(errInvalidNetworkNameNs))
	}
	logger.Info("get subnet", "providerType", providerType, "region", region)
	if providerType == consts.ProviderTypeGKE {
		// gke do not need subnet
		return nil, nil
	}
	//ns like nightly-ms-12345 or prod-12345
	//we should get project id from ns last part like 12345
	namespaces := strings.Split(network.Namespace, "-")
	if len(namespaces) < 2 {
		return nil, apperror.NewInternalError(errors.New(errInvalidNetworkNameNs))
	}
	projectstr := namespaces[len(namespaces)-1]
	logger.Info("get subnet", "project", projectstr)
	querydata := map[string]string{
		"providerName": providerType,
		"regionName":   region,
	}
	resp, code, err := common.GetGlobalHTTP[models.Globaldedicatedv1CompatNetwork](ctx, cnc.Env, networkurl, projectstr, &querydata)
	if err != nil {
		if code == int(bizerr.ErrCodeRecordNotFound) {
			return nil, apperror.NewNotFound(schema.GroupResource{
				Group:    network.GetObjectKind().GroupVersionKind().Group,
				Resource: network.GetObjectKind().GroupVersionKind().Kind,
			}, network.Name)
		}
		return nil, apperror.NewInternalError(err)
	}
	if code != http.StatusOK {
		if code == http.StatusNotFound && resp == nil {
			return nil, apperror.NewNotFound(schema.GroupResource{
				Group:    network.GetObjectKind().GroupVersionKind().Group,
				Resource: network.GetObjectKind().GroupVersionKind().Kind,
			}, network.Name)
		}
		logger.Info("call global subnet failed", "code", code)
		return nil, apperror.NewInternalError(fmt.Errorf("call global subnet failed, code=%d", code))
	}
	awsinfo := resp.Aws
	return awsinfo.PrivateSubnets, nil
}
