/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package aliup

import (
	"errors"
	"fmt"
	"os"
	"time"

	"k8s.io/klog/v2"

	alicloud "github.com/aliyun/alibaba-cloud-sdk-go/sdk"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
	slbnew "github.com/aliyun/alibaba-cloud-sdk-go/services/slb"
	"github.com/denverdino/aliyungo/common"
	"github.com/denverdino/aliyungo/ecs"
	"github.com/denverdino/aliyungo/ess"
	"github.com/denverdino/aliyungo/ram"
	"github.com/denverdino/aliyungo/slb"

	prj "k8s.io/kops"
	"k8s.io/kops/dnsprovider/pkg/dnsprovider"
	"k8s.io/kops/pkg/apis/kops"
	"k8s.io/kops/pkg/cloudinstances"
	"k8s.io/kops/upup/pkg/fi"
)

const (
	TagClusterName           = "KubernetesCluster"
	TagNameRolePrefix        = "k8s.io/role/"
	TagNameEtcdClusterPrefix = "k8s.io/etcd/"
	TagRoleMaster            = "master"
)

// This is for statistic purpose.
var KubernetesKopsIdentity = fmt.Sprintf("Kubernetes.Kops/%s", prj.Version)

type ALICloud interface {
	fi.Cloud

	// Clients that use official Alicloud go sdk
	SLB() *slbnew.Client

	// Since package github.com/denverdino/aliyungo is not actively maintained,
	// clients that use github.com/denverdino/aliyungo, will be deprecated after
	// the above official SDK clients covers all of them
	EcsClient() *ecs.Client
	SlbClient() *slb.Client
	RamClient() *ram.RamClient
	EssClient() *ess.Client
	VpcClient() *ecs.Client

	AddClusterTags(tags map[string]string)
	GetTags(resourceId string, resourceType string) (map[string]string, error)
	CreateTags(resourceId string, resourceType string, tags map[string]string) error
	RemoveTags(resourceId string, resourceType string, tags map[string]string) error
	GetClusterTags() map[string]string
	FindClusterStatus(cluster *kops.Cluster) (*kops.ClusterStatus, error)
	GetApiIngressStatus(cluster *kops.Cluster) ([]kops.ApiIngressStatus, error)
}

type aliCloudImplementation struct {
	slb *slbnew.Client

	ecsClient *ecs.Client
	slbClient *slb.Client
	ramClient *ram.RamClient
	essClient *ess.Client
	vpcClient *ecs.Client

	region string
	tags   map[string]string
}

var _ fi.Cloud = &aliCloudImplementation{}

// NewALICloud returns a Cloud, expecting the env vars ALIYUN_ACCESS_KEY_ID && ALIYUN_ACCESS_KEY_SECRET
// NewALICloud will return an err if env vars are not defined
func NewALICloud(region string, tags map[string]string) (ALICloud, error) {

	c := &aliCloudImplementation{region: region}

	accessKeyID := os.Getenv("ALIYUN_ACCESS_KEY_ID")
	if accessKeyID == "" {
		return nil, errors.New("ALIYUN_ACCESS_KEY_ID is required")
	}
	accessKeySecret := os.Getenv("ALIYUN_ACCESS_KEY_SECRET")
	if accessKeySecret == "" {
		return nil, errors.New("ALIYUN_ACCESS_KEY_SECRET is required")
	}

	c.ecsClient = ecs.NewClient(accessKeyID, accessKeySecret)
	c.ecsClient.SetUserAgent(KubernetesKopsIdentity)
	c.slbClient = slb.NewClient(accessKeyID, accessKeySecret)
	ramclient := ram.NewClient(accessKeyID, accessKeySecret)
	c.ramClient = ramclient.(*ram.RamClient)
	c.essClient = ess.NewClient(accessKeyID, accessKeySecret)
	c.vpcClient = ecs.NewVPCClient(accessKeyID, accessKeySecret, common.Region(region))
	c.tags = tags

	// With Alicloud official go SDK
	var err error

	config := alicloud.NewConfig()
	credential := credentials.NewAccessKeyCredential(accessKeyID, accessKeySecret)

	c.slb, err = slbnew.NewClientWithOptions(region, config, credential)
	if err != nil {
		return nil, fmt.Errorf("error creating slb sdk client: %v", err)
	}

	return c, nil
}

func (c *aliCloudImplementation) SLB() *slbnew.Client {
	return c.slb
}

// Clients as below are with github.com/denverdino/aliyungo
func (c *aliCloudImplementation) EcsClient() *ecs.Client {
	return c.ecsClient
}

func (c *aliCloudImplementation) SlbClient() *slb.Client {
	return c.slbClient
}

func (c *aliCloudImplementation) RamClient() *ram.RamClient {
	return c.ramClient
}

func (c *aliCloudImplementation) EssClient() *ess.Client {
	return c.essClient
}

func (c *aliCloudImplementation) VpcClient() *ecs.Client {
	return c.vpcClient
}

func (c *aliCloudImplementation) Region() string {
	return c.region
}

func (c *aliCloudImplementation) ProviderID() kops.CloudProviderID {
	return kops.CloudProviderALI
}

func (c *aliCloudImplementation) DNS() (dnsprovider.Interface, error) {
	return nil, errors.New("DNS not implemented on aliCloud")
}

func (c *aliCloudImplementation) DeleteGroup(g *cloudinstances.CloudInstanceGroup) error {
	return errors.New("DeleteGroup not implemented on aliCloud")
}

func (c *aliCloudImplementation) DeleteInstance(i *cloudinstances.CloudInstance) error {
	id := i.ID
	if id == "" {
		return fmt.Errorf("id was not set on CloudInstance: %v", i)
	}

	if err := c.EcsClient().StopInstance(id, false); err != nil {
		return fmt.Errorf("error stopping instance %q: %v", id, err)
	}

	// Wait for 3 min to stop the instance
	for i := 0; i < 36; i++ {
		ins, err := c.EcsClient().DescribeInstanceAttribute(id)
		if err != nil {
			return fmt.Errorf("error describing instance %q: %v", id, err)
		}

		klog.V(8).Infof("stopping Alicloud ecs instance %q, current Status: %q", id, ins.Status)
		time.Sleep(time.Second * 5)

		if ins.Status == ecs.Stopped {
			break
		}

		if i == 35 {
			return fmt.Errorf("fail to stop ecs instance %s in 3 mins", id)
		}
	}

	if err := c.EcsClient().DeleteInstance(id); err != nil {
		return fmt.Errorf("error deleting instance %q: %v", id, err)
	}

	klog.V(8).Infof("deleted Alicloud ecs instance %q", id)

	return nil
}

func (c *aliCloudImplementation) DetachInstance(i *cloudinstances.CloudInstance) error {
	return errors.New("aliCloud cloud provider does not support surging")
}

func (c *aliCloudImplementation) FindVPCInfo(id string) (*fi.VPCInfo, error) {
	request := &ecs.DescribeVpcsArgs{
		RegionId: common.Region(c.Region()),
		VpcId:    id,
	}
	vpcs, _, err := c.EcsClient().DescribeVpcs(request)
	if err != nil {
		return nil, fmt.Errorf("error listing VPCs: %v", err)
	}

	if len(vpcs) != 1 {
		return nil, fmt.Errorf("found multiple VPCs for %q", id)
	}
	vpcInfo := &fi.VPCInfo{
		CIDR: vpcs[0].CidrBlock,
	}

	describeVSwitchesArgs := &ecs.DescribeVSwitchesArgs{
		VpcId:    id,
		RegionId: common.Region(c.Region()),
	}
	vswitchList, _, err := c.EcsClient().DescribeVSwitches(describeVSwitchesArgs)
	if err != nil {
		return nil, fmt.Errorf("error listing VSwitchs: %v", err)
	}

	for _, vswitch := range vswitchList {
		s := &fi.SubnetInfo{
			ID:   vswitch.VSwitchId,
			Zone: vswitch.ZoneId,
			CIDR: vswitch.CidrBlock,
		}
		vpcInfo.Subnets = append(vpcInfo.Subnets, s)
	}

	return vpcInfo, nil

}

// GetTags will get the specified resource's tags.
func (c *aliCloudImplementation) GetTags(resourceId string, resourceType string) (map[string]string, error) {
	if resourceId == "" {
		return nil, errors.New("resourceId not provided to GetTags")
	}
	tags := map[string]string{}

	request := &ecs.DescribeTagsArgs{
		RegionId:     common.Region(c.Region()),
		ResourceType: ecs.TagResourceType(resourceType), //image, instance, snapshot or disk
		ResourceId:   resourceId,
	}
	responseTags, _, err := c.EcsClient().DescribeTags(request)
	if err != nil {
		return tags, fmt.Errorf("error getting tags on %v: %v", resourceId, err)
	}

	for _, tag := range responseTags {
		tags[tag.TagKey] = tag.TagValue
	}
	return tags, nil

}

// AddClusterTags will add ClusterTags to resources (in ALI, only disk, instance, snapshot or image can be tagged )
func (c *aliCloudImplementation) AddClusterTags(tags map[string]string) {

	if c.tags != nil && len(c.tags) != 0 && tags != nil {
		for k, v := range c.tags {
			tags[k] = v
		}
	}
}

// CreateTags will add tags to the specified resource.
func (c *aliCloudImplementation) CreateTags(resourceId string, resourceType string, tags map[string]string) error {
	if len(tags) == 0 {
		return nil
	} else if len(tags) > 10 {
		klog.V(4).Infof("The number of specified resource's tags exceeds 10, resourceId:%q", resourceId)
	}
	if resourceId == "" {
		return errors.New("resourceId not provided to CreateTags")
	}
	if resourceType == "" {
		return errors.New("resourceType not provided to CreateTags")
	}

	request := &ecs.AddTagsArgs{
		ResourceId:   resourceId,
		ResourceType: ecs.TagResourceType(resourceType), //image, instance, snapshot or disk
		RegionId:     common.Region(c.Region()),
		Tag:          tags,
	}
	err := c.EcsClient().AddTags(request)
	if err != nil {
		return fmt.Errorf("error creating tags on %v: %v", resourceId, err)
	}

	return nil
}

// RemoveTags will remove tags from the specified resource.
func (c *aliCloudImplementation) RemoveTags(resourceId string, resourceType string, tags map[string]string) error {
	if len(tags) == 0 {
		return nil
	}
	if resourceId == "" {
		return errors.New("resourceId not provided to RemoveTags")
	}
	if resourceType == "" {
		return errors.New("resourceType not provided to RemoveTags")
	}

	request := &ecs.RemoveTagsArgs{
		ResourceId:   resourceId,
		ResourceType: ecs.TagResourceType(resourceType), //image, instance, snapshot or disk
		RegionId:     common.Region(c.Region()),
		Tag:          tags,
	}
	err := c.EcsClient().RemoveTags(request)
	if err != nil {
		return fmt.Errorf("error removing tags on %v: %v", resourceId, err)
	}

	return nil
}

// GetClusterTags will get the ClusterTags
func (c *aliCloudImplementation) GetClusterTags() map[string]string {
	return c.tags
}

func (c *aliCloudImplementation) GetApiIngressStatus(cluster *kops.Cluster) ([]kops.ApiIngressStatus, error) {
	var ingresses []kops.ApiIngressStatus
	name := "api." + cluster.Name

	describeLoadBalancersArgs := &slb.DescribeLoadBalancersArgs{
		RegionId:         common.Region(c.Region()),
		LoadBalancerName: name,
	}

	responseLoadBalancers, err := c.SlbClient().DescribeLoadBalancers(describeLoadBalancersArgs)
	if err != nil {
		return nil, fmt.Errorf("error finding LoadBalancers: %v", err)
	}
	// Don't exist loadbalancer with specified ClusterTags or Name.
	if len(responseLoadBalancers) == 0 {
		return nil, nil
	}
	if len(responseLoadBalancers) > 1 {
		klog.V(4).Infof("The number of specified loadbalancer with the same name exceeds 1, loadbalancerName:%q", name)
	}

	address := responseLoadBalancers[0].Address
	ingresses = append(ingresses, kops.ApiIngressStatus{IP: address})

	return ingresses, nil
}

func ZoneToVSwitchID(VPCID string, zones []string, vswitchIDs []string) (map[string]string, error) {
	regionId, err := getRegionByZones(zones)
	if err != nil {
		return nil, err
	}

	res := make(map[string]string)
	cloudTags := map[string]string{}
	aliCloud, err := NewALICloud(regionId, cloudTags)
	if err != nil {
		return res, fmt.Errorf("error loading cloud: %v", err)
	}

	describeVpcsArgs := &ecs.DescribeVpcsArgs{
		RegionId: common.Region(regionId),
		VpcId:    VPCID,
	}

	vpc, _, err := aliCloud.EcsClient().DescribeVpcs(describeVpcsArgs)
	if err != nil {
		return res, fmt.Errorf("error describing VPC: %v", err)
	}

	if len(vpc) == 0 {
		return res, fmt.Errorf("VPC %q not found", VPCID)
	}

	if len(vpc) != 1 {
		return nil, fmt.Errorf("found multiple VPCs for %q", VPCID)
	}
	subnetByID := make(map[string]string)
	for _, VSId := range vpc[0].VSwitchIds.VSwitchId {
		subnetByID[VSId] = VSId
	}

	for _, VSwitchId := range vswitchIDs {

		_, ok := subnetByID[VSwitchId]
		if !ok {
			return res, fmt.Errorf("vswitch %s not found in VPC %s", VSwitchId, VPCID)
		}
		describeVSwitchesArgs := &ecs.DescribeVSwitchesArgs{
			VpcId:     vpc[0].VpcId,
			RegionId:  common.Region(regionId),
			VSwitchId: VSwitchId,
		}

		vswitchList, _, err := aliCloud.EcsClient().DescribeVSwitches(describeVSwitchesArgs)
		if err != nil {
			return nil, fmt.Errorf("error listing VSwitchs: %v", err)
		}

		if len(vswitchList) == 0 {
			return nil, fmt.Errorf("VSwitch %q not found", VSwitchId)
		}

		if len(vswitchList) != 1 {
			return nil, fmt.Errorf("found multiple VSwitchs for %q", VSwitchId)
		}

		zone := vswitchList[0].ZoneId
		if res[zone] != "" {
			return res, fmt.Errorf("vswitch %s and %s have the same zone", vswitchList[0].VSwitchId, zone)
		}
		res[zone] = vswitchList[0].VSwitchId

	}
	return res, nil
}
