package aliyun

import (
	"fmt"
	"os"

	cs "github.com/alibabacloud-go/cs-20151215/v5/client"
	openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
	ecs "github.com/alibabacloud-go/ecs-20140526/v4/client"
	nlb "github.com/alibabacloud-go/nlb-20220430/v4/client"
	privatelink "github.com/alibabacloud-go/privatelink-20200415/v5/client"
	slb "github.com/alibabacloud-go/slb-20140515/v4/client"
	sts "github.com/alibabacloud-go/sts-20150401/v2/client"
	vpc "github.com/alibabacloud-go/vpc-20160428/v6/client"
	oldsdk "github.com/aliyun/alibaba-cloud-sdk-go/sdk"
	oldauth "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth"
	oldcredentials "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
	"github.com/aliyun/alibaba-cloud-sdk-go/services/vpcpeer"
	"github.com/aliyun/credentials-go/credentials"
	"k8s.io/utils/ptr"
)

type Option func(*openapi.Config)

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

//go:generate mockgen -package aliyun -source=client.go -destination=client_mock.go
type Client interface {
	CS(*AssumedRole, ...Option) (*cs.Client, error)
	ECS(*AssumedRole, ...Option) (*ecs.Client, error)
	VpcPeer(*AssumedRole, ...Option) (*vpcpeer.Client, error)
	VPC(*AssumedRole, ...Option) (*vpc.Client, error)
	PrivateLink(*AssumedRole, ...Option) (*privatelink.Client, error)
	NLB(*AssumedRole, ...Option) (*nlb.Client, error)
	SLB(*AssumedRole, ...Option) (*slb.Client, error)
}

type clientset struct{}

func NewClientSet() Client {
	return &clientset{}
}

func (c *clientset) CS(ar *AssumedRole, opts ...Option) (*cs.Client, error) {
	cfg, err := buildConfig(ar, opts...)
	if err != nil {
		return nil, err
	}
	return cs.NewClient(cfg)
}

func (c *clientset) ECS(ar *AssumedRole, opts ...Option) (*ecs.Client, error) {
	cfg, err := buildConfig(ar, opts...)
	if err != nil {
		return nil, err
	}
	if cfg.GetRegionId() != nil {
		cfg.SetEndpoint(fmt.Sprintf("ecs.%s.aliyuncs.com", *cfg.GetRegionId()))
	}
	return ecs.NewClient(cfg)
}

func (c *clientset) PrivateLink(ar *AssumedRole, opts ...Option) (*privatelink.Client, error) {
	cfg, err := buildConfig(ar, opts...)
	if err != nil {
		return nil, err
	}
	if cfg.GetRegionId() != nil {
		cfg.SetEndpoint(fmt.Sprintf("privatelink-vpc.%s.aliyuncs.com", *cfg.GetRegionId()))
	}
	return privatelink.NewClient(cfg)
}

func (c *clientset) NLB(ar *AssumedRole, opts ...Option) (*nlb.Client, error) {
	cfg, err := buildConfig(ar, opts...)
	if err != nil {
		return nil, err
	}
	if cfg.GetRegionId() != nil {
		cfg.SetEndpoint(fmt.Sprintf("nlb-vpc.%s.aliyuncs.com", *cfg.GetRegionId()))
	}
	return nlb.NewClient(cfg)
}

func (c *clientset) SLB(ar *AssumedRole, opts ...Option) (*slb.Client, error) {
	cfg, err := buildConfig(ar, opts...)
	if err != nil {
		return nil, err
	}
	if cfg.GetRegionId() != nil {
		cfg.SetEndpoint(fmt.Sprintf("slb-vpc.%s.aliyuncs.com", *cfg.GetRegionId()))
	}
	return slb.NewClient(cfg)
}

func (c *clientset) VPC(ar *AssumedRole, opts ...Option) (*vpc.Client, error) {
	cfg, err := buildConfig(ar, opts...)
	if err != nil {
		return nil, err
	}
	if cfg.GetRegionId() != nil {
		cfg.SetEndpoint(fmt.Sprintf("vpc.%s.aliyuncs.com", *cfg.GetRegionId()))
	}
	return vpc.NewClient(cfg)
}

func (c *clientset) VpcPeer(ar *AssumedRole, opts ...Option) (*vpcpeer.Client, error) {
	cfg, err := buildConfig(ar, opts...)
	if err != nil {
		return nil, err
	}

	region := ""
	if cfg.GetRegionId() != nil {
		region = *cfg.GetRegionId()
	}

	var credential oldauth.Credential
	if cfg.GetSecurityToken() != nil && *cfg.GetSecurityToken() != "" {
		credential = oldcredentials.NewStsTokenCredential(*cfg.GetAccessKeyId(), *cfg.GetAccessKeySecret(), *cfg.GetSecurityToken())
	} else {
		credential = oldcredentials.NewAccessKeyCredential(*cfg.GetAccessKeyId(), *cfg.GetAccessKeySecret())
	}

	return vpcpeer.NewClientWithOptions(region, oldsdk.NewConfig(), credential)
}

func buildConfig(ar *AssumedRole, opts ...Option) (*openapi.Config, error) {
	config, err := GetDefaultCredentialConfig(opts...)
	if err != nil {
		return nil, err
	}
	// No assumed role provided, return the default credential provider config
	if ar == nil || ar.RoleARN == "" {
		return config, nil
	}
	return getStsCredentialConfig(config, ar, opts...)
}

func GetDefaultCredentialConfig(opts ...Option) (*openapi.Config, error) {
	//Notice: use OIDC way directly instead of using the default credential provider chain, which may fail since it may get the wrong RAM role.
	credConfig := new(credentials.Config).
		SetType("oidc_role_arn").
		SetOIDCProviderArn(os.Getenv("ALIBABA_CLOUD_OIDC_PROVIDER_ARN")).
		SetOIDCTokenFilePath(os.Getenv("ALIBABA_CLOUD_OIDC_TOKEN_FILE")).
		SetRoleArn(os.Getenv("ALIBABA_CLOUD_ROLE_ARN"))
	provider, err := credentials.NewCredential(credConfig)
	if err != nil {
		return nil, err
	}
	cred, err := provider.GetCredential()
	if err != nil {
		return nil, err
	}
	config := new(openapi.Config).
		SetAccessKeyId(*cred.AccessKeyId).
		SetAccessKeySecret(*cred.AccessKeySecret).
		SetSecurityToken(*cred.SecurityToken)
	for _, opt := range opts {
		opt(config)
	}
	return config, nil
}

func getStsCredentialConfig(config *openapi.Config, ar *AssumedRole, opts ...Option) (*openapi.Config, error) {
	// Use STS to assume role and retrieve assumed role credentials
	stsClient, err := sts.NewClient(config)
	if err != nil {
		return nil, err
	}
	req := &sts.AssumeRoleRequest{
		RoleArn:         ptr.To(ar.RoleARN),
		ExternalId:      ar.ExternalID,
		RoleSessionName: ptr.To("dataplane-manager"),
	}
	resp, err := stsClient.AssumeRole(req)
	if err != nil {
		return nil, err
	}
	cfg := new(openapi.Config).
		SetAccessKeyId(*resp.Body.Credentials.AccessKeyId).
		SetAccessKeySecret(*resp.Body.Credentials.AccessKeySecret).
		SetSecurityToken(*resp.Body.Credentials.SecurityToken)
	for _, opt := range opts {
		opt(cfg)
	}
	return cfg, nil
}

func WithRegion(region string) Option {
	return func(cfg *openapi.Config) {
		cfg.SetRegionId(region)
	}
}
