package procedure

import (
	"context"
	"crypto/md5"
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/arn"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/autoscaling"
	"github.com/aws/aws-sdk-go/service/autoscaling/autoscalingiface"
	"github.com/aws/aws-sdk-go/service/ec2"
	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
	"github.com/aws/aws-sdk-go/service/elbv2"
	"github.com/aws/aws-sdk-go/service/elbv2/elbv2iface"
	"github.com/aws/aws-sdk-go/service/lambda"
	"github.com/aws/aws-sdk-go/service/lambda/lambdaiface"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3iface"
	"github.com/aws/aws-sdk-go/service/sts"
	"github.com/aws/aws-sdk-go/service/sts/stsiface"
	cpv1beta1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	xpv1beta1 "github.com/crossplane-contrib/provider-aws/apis/v1beta1"
	cpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
)

const (
	ProcedureConfigmap = "procedure-configmap"
	ProcedureNamespace = "procedure-system"

	AnnotationKeyEndpointDNS         = "infra.pingcap.com/endpoint-dns"
	AnnotationKeyFrpsSvcName         = "infra.pingcap.com/frps-svc-name"
	AnnotationKeyFrpsInternalSvcName = "infra.pingcap.com/frps-svc-name-internal"
	AnnotationKeyFrpsExternalSvcName = "infra.pingcap.com/frps-svc-name-external"
	AnnotationKeyFrpsSvcNs           = "infra.pingcap.com/frps-svc-namespace"
)

// getEKSObject returns the EKS cluster object for the connection.
func getEKSObject(ctx context.Context, cli client.Reader, conn *eksv1alpha1.EKSConnection) (*cpv1beta1.Cluster, error) {
	key := types.NamespacedName{
		Name: conn.Spec.Target.Name,
	}
	eks := &cpv1beta1.Cluster{}
	err := cli.Get(ctx, key, eks)
	if err != nil {
		return nil, err
	}
	return eks, nil
}

// GetProcedureInfo returns the cluster info which identifies the target EKS.
func GetProcedureInfo(ctx context.Context, cli client.Client, conn *eksv1alpha1.EKSConnection, namespace string) (ProcedureInfo, error) {
	eks, err := getEKSObject(ctx, cli, conn)
	if err != nil {
		return ProcedureInfo{}, err
	}
	if namespace == "" {
		namespace = ProcedureNamespace
	}
	if eks.Status.AtProvider.ResourcesVpcConfig.VpcID == "" {
		return ProcedureInfo{}, fmt.Errorf("eks cluster %s doesn't have vpc id", eks.Name)
	}
	tenantProviderConfig := &xpv1beta1.ProviderConfig{}
	err = cli.Get(ctx, types.NamespacedName{Name: conn.Spec.TenantProviderConfig}, tenantProviderConfig)
	if err != nil {
		return ProcedureInfo{}, fmt.Errorf("failed getting tenant's provider config '%s': %v", conn.Spec.TenantProviderConfig, err)
	}
	return ProcedureInfo{
		Region:                aws.StringValue(eks.Spec.ForProvider.Region),
		Cluster:               eks.Name,
		ProviderConfigRef:     conn.Spec.TenantProviderConfig,
		HostProviderConfigRef: conn.Spec.HostProviderConfig,
		VpcID:                 eks.Status.AtProvider.ResourcesVpcConfig.VpcID,
		SubnetIDs:             eks.Spec.ForProvider.ResourcesVpcConfig.SubnetIDs,
		SecurityGroupIDs:      eks.Spec.ForProvider.ResourcesVpcConfig.SecurityGroupIDs,
		eksConnection:         conn,
		namespace:             namespace,
		assumeRole:            aws.StringValue(tenantProviderConfig.Spec.AssumeRoleARN),
		externalID:            aws.StringValue(tenantProviderConfig.Spec.ExternalID),
		eks:                   eks,
	}, nil
}

func StringHash(s string) string {
	return fmt.Sprintf("%x", md5.Sum([]byte(s)))
}

// getConfigKey returns the object key for the procedures configmap.
func getConfigKey(info ProcedureInfo) types.NamespacedName {
	return types.NamespacedName{
		Namespace: info.namespace,
		Name:      ProcedureConfigmap,
	}
}

// GetConfigWithKey returns the value for corresponding key in the procedure configmap.
func GetConfigWithKey(ctx context.Context, cli client.Reader, info ProcedureInfo, key string) (string, error) {
	cm := &corev1.ConfigMap{}
	err := cli.Get(ctx, getConfigKey(info), cm)
	if err != nil {
		return "", err
	}
	errNotExist := fmt.Errorf("key %q doesn't exist in procedure configmap", key)
	if cm.Data != nil {
		if value, exist := cm.Data[key]; !exist {
			return "", errNotExist
		} else {
			return value, nil
		}
	}
	return "", errNotExist
}

// SetConfigKeyValue sets key value pair in configmap.
func SetConfigWithKey(ctx context.Context, cli client.Client, info ProcedureInfo, key, value string) error {
	cm, objkey := &corev1.ConfigMap{}, getConfigKey(info)
	err := cli.Get(ctx, objkey, cm)
	if err != nil && !errors.IsNotFound(err) {
		return err
	}
	// create configmap if not existed
	if err != nil {
		cm = &corev1.ConfigMap{}
		cm.Name = objkey.Name
		cm.Namespace = objkey.Namespace
		cm.Data = map[string]string{key: value}
		err = cli.Create(ctx, cm)
		if err != nil {
			return err
		}
		return nil
	}

	if cm.Data == nil {
		cm.Data = make(map[string]string)
	}
	cm.Data[key] = value
	return cli.Update(ctx, cm)
}

// GetCrossplaneProvider returns the crossplane provider for the tenant.
func GetCrossplaneProvider(info ProcedureInfo) *cpv1.Reference {
	return &cpv1.Reference{
		Name: info.ProviderConfigRef,
	}
}

// IsCrossplaneObjReady returns true if the crossplane object is ready.
func IsCrossplaneObjReady(status cpv1.ConditionedStatus) bool {
	return status.GetCondition(cpv1.TypeReady).Status == corev1.ConditionTrue
}

// IsAlreadyOwnedError returns true if the error is an AlreadyOwnedError.
func IsAlreadyOwnedError(err error) bool {
	switch err.(type) {
	case *controllerutil.AlreadyOwnedError:
		return true
	}
	return false
}

// ConditionEqual checks if the condition is equal to the given status except for the lastTransitionTime.
func ConditionEqual(cond1, cond2 eksv1alpha1.Condition) bool {
	return cond1.Type == cond2.Type &&
		cond1.Status == cond2.Status &&
		cond1.Reason == cond2.Reason &&
		cond1.Message == cond2.Message
}

// privateLinkMngr provides utility functions for managing private links.
type privateLinkMngr interface {
	getEndpointServiceName(ctx context.Context, cli client.Reader, info ProcedureInfo, epSvcName string) (string, error)
	getEndpointDNS(ctx context.Context, cli client.Reader, epName string) (string, error)
	acceptPrivateLink(ctx context.Context, endpointSvcID, endpointID string) error
}

// getRootPrincipal returns the root principal for the given tenant role ARN.
func getRootPrincipal(arnStr string) (string, error) {
	parsedArn, err := arn.Parse(arnStr)
	if err != nil {
		return "", err
	}
	parsedArn.Resource = "root"
	return parsedArn.String(), nil
}

func (info *ProcedureInfo) getAssumeRole() *AssumedRole {
	return &AssumedRole{
		RoleARN:    info.assumeRole,
		ExternalID: info.externalID,
	}
}

// AssumedRole specifies the assumed role for issuing AWS requests.
type AssumedRole struct {
	RoleARN    string
	ExternalID string
}

func assumeRoleCred(sess *session.Session, role *AssumedRole) *credentials.Credentials {
	return stscreds.NewCredentials(sess, role.RoleARN, func(provider *stscreds.AssumeRoleProvider) {
		provider.ExternalID = aws.String(role.ExternalID)
	})
}

// AWSService returns the AWS service client.
type AWSService interface {
	EC2API(role *AssumedRole) ec2iface.EC2API
	ELBV2API(role *AssumedRole) elbv2iface.ELBV2API
	LambdaAPI(role *AssumedRole) lambdaiface.LambdaAPI
	StsAPI(role *AssumedRole) stsiface.STSAPI
	AutoScalingAPI(role *AssumedRole) autoscalingiface.AutoScalingAPI
	S3API(role *AssumedRole) s3iface.S3API
}

type awsSvc struct {
	sess *session.Session
}

func (a *awsSvc) EC2API(role *AssumedRole) ec2iface.EC2API {
	if role == nil {
		return ec2.New(a.sess)
	}
	return ec2.New(a.sess, &aws.Config{
		Credentials: assumeRoleCred(a.sess, role),
	})
}

func (a *awsSvc) ELBV2API(role *AssumedRole) elbv2iface.ELBV2API {
	if role == nil {
		return elbv2.New(a.sess)
	}
	return elbv2.New(a.sess, &aws.Config{
		Credentials: assumeRoleCred(a.sess, role),
	})
}

func (a *awsSvc) LambdaAPI(role *AssumedRole) lambdaiface.LambdaAPI {
	if role == nil {
		return lambda.New(a.sess)
	}
	return lambda.New(a.sess, &aws.Config{
		Credentials: assumeRoleCred(a.sess, role),
	})
}

func (a *awsSvc) StsAPI(role *AssumedRole) stsiface.STSAPI {
	if role == nil {
		return sts.New(a.sess)
	}
	return sts.New(a.sess, &aws.Config{
		Credentials: assumeRoleCred(a.sess, role),
	})
}

func (a *awsSvc) AutoScalingAPI(role *AssumedRole) autoscalingiface.AutoScalingAPI {
	if role == nil {
		return autoscaling.New(a.sess)
	}
	return autoscaling.New(a.sess, &aws.Config{
		Credentials: assumeRoleCred(a.sess, role),
	})
}

func (a *awsSvc) S3API(role *AssumedRole) s3iface.S3API {
	if role == nil {
		return s3.New(a.sess)
	}
	return s3.New(a.sess, &aws.Config{
		Credentials: assumeRoleCred(a.sess, role),
	})
}

func NewAWSService(sess *session.Session) *awsSvc {
	return &awsSvc{sess: sess}
}

var _ AWSService = &awsSvc{}

func NetworkAssumeRole(network *cloudv1.Network) (*AssumedRole, error) {
	spec, err := eksv1alpha1.EKSNetworkSpecForNetwork(network)
	if err != nil {
		return nil, err
	}

	return &AssumedRole{
		RoleARN:    spec.AssumeRoleArn,
		ExternalID: spec.AssumeExternalId,
	}, nil
}

// AsReader drops k8s client privilege and returns the reader.
func AsReader(cli client.Client) client.Reader {
	return cli.(client.Reader)
}
