//go:generate struct-markdown

package ecs

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"runtime"
	"time"

	"github.com/aliyun/alibaba-cloud-sdk-go/services/ecs"
	"github.com/hashicorp/packer/builder/alicloud/version"
	"github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate"
	"github.com/mitchellh/go-homedir"
)

// Config of alicloud
type AlicloudAccessConfig struct {
	// Alicloud access key must be provided unless `profile` is set, but it can
	// also be sourced from the `ALICLOUD_ACCESS_KEY` environment variable.
	AlicloudAccessKey string `mapstructure:"access_key" required:"true"`
	// Alicloud secret key must be provided unless `profile` is set, but it can
	// also be sourced from the `ALICLOUD_SECRET_KEY` environment variable.
	AlicloudSecretKey string `mapstructure:"secret_key" required:"true"`
	// Alicloud region must be provided unless `profile` is set, but it can
	// also be sourced from the `ALICLOUD_REGION` environment variable.
	AlicloudRegion string `mapstructure:"region" required:"true"`
	// The region validation can be skipped if this value is true, the default
	// value is false.
	AlicloudSkipValidation bool `mapstructure:"skip_region_validation" required:"false"`
	// The image validation can be skipped if this value is true, the default
	// value is false.
	AlicloudSkipImageValidation bool `mapstructure:"skip_image_validation" required:"false"`
	// Alicloud profile must be set unless `access_key` is set; it can also be
	// sourced from the `ALICLOUD_PROFILE` environment variable.
	AlicloudProfile string `mapstructure:"profile" required:"false"`
	// Alicloud shared credentials file path. If this file exists, access and
	// secret keys will be read from this file.
	AlicloudSharedCredentialsFile string `mapstructure:"shared_credentials_file" required:"false"`
	// STS access token, can be set through template or by exporting as
	// environment variable such as `export SECURITY_TOKEN=value`.
	SecurityToken string `mapstructure:"security_token" required:"false"`

	client *ClientWrapper
}

const Packer = "HashiCorp-Packer"
const DefaultRequestReadTimeout = 10 * time.Second

// Client for AlicloudClient
func (c *AlicloudAccessConfig) Client() (*ClientWrapper, error) {
	if c.client != nil {
		return c.client, nil
	}
	if c.SecurityToken == "" {
		c.SecurityToken = os.Getenv("SECURITY_TOKEN")
	}

	var getProviderConfig = func(str string, key string) string {
		value, err := getConfigFromProfile(c, key)
		if err == nil && value != nil {
			str = value.(string)
		}
		return str
	}

	if c.AlicloudAccessKey == "" || c.AlicloudSecretKey == "" {
		c.AlicloudAccessKey = getProviderConfig(c.AlicloudAccessKey, "access_key_id")
		c.AlicloudSecretKey = getProviderConfig(c.AlicloudSecretKey, "access_key_secret")
		c.AlicloudRegion = getProviderConfig(c.AlicloudRegion, "region_id")
		c.SecurityToken = getProviderConfig(c.SecurityToken, "sts_token")
	}

	client, err := ecs.NewClientWithStsToken(c.AlicloudRegion, c.AlicloudAccessKey, c.AlicloudSecretKey, c.SecurityToken)
	if err != nil {
		return nil, err
	}

	client.AppendUserAgent(Packer, version.AlicloudPluginVersion.FormattedVersion())
	client.SetReadTimeout(DefaultRequestReadTimeout)
	c.client = &ClientWrapper{client}

	return c.client, nil
}

func (c *AlicloudAccessConfig) Prepare(ctx *interpolate.Context) []error {
	var errs []error
	if err := c.Config(); err != nil {
		errs = append(errs, err)
	}

	if c.AlicloudRegion == "" {
		c.AlicloudRegion = os.Getenv("ALICLOUD_REGION")
	}

	if c.AlicloudRegion == "" {
		errs = append(errs, fmt.Errorf("region option or ALICLOUD_REGION must be provided in template file or environment variables."))
	}

	if len(errs) > 0 {
		return errs
	}

	return nil
}

func (c *AlicloudAccessConfig) Config() error {
	if c.AlicloudAccessKey == "" {
		c.AlicloudAccessKey = os.Getenv("ALICLOUD_ACCESS_KEY")
	}
	if c.AlicloudSecretKey == "" {
		c.AlicloudSecretKey = os.Getenv("ALICLOUD_SECRET_KEY")
	}
	if c.AlicloudProfile == "" {
		c.AlicloudProfile = os.Getenv("ALICLOUD_PROFILE")
	}
	if c.AlicloudSharedCredentialsFile == "" {
		c.AlicloudSharedCredentialsFile = os.Getenv("ALICLOUD_SHARED_CREDENTIALS_FILE")
	}
	if (c.AlicloudAccessKey == "" || c.AlicloudSecretKey == "") && c.AlicloudProfile == "" {
		return fmt.Errorf("ALICLOUD_ACCESS_KEY and ALICLOUD_SECRET_KEY must be set in template file or environment variables.")
	}
	return nil

}

func (c *AlicloudAccessConfig) ValidateRegion(region string) error {

	supportedRegions, err := c.getSupportedRegions()
	if err != nil {
		return err
	}

	for _, supportedRegion := range supportedRegions {
		if region == supportedRegion {
			return nil
		}
	}

	return fmt.Errorf("Not a valid alicloud region: %s", region)
}

func (c *AlicloudAccessConfig) getSupportedRegions() ([]string, error) {
	client, err := c.Client()
	if err != nil {
		return nil, err
	}

	regionsRequest := ecs.CreateDescribeRegionsRequest()
	regionsResponse, err := client.DescribeRegions(regionsRequest)
	if err != nil {
		return nil, err
	}

	validRegions := make([]string, len(regionsResponse.Regions.Region))
	for _, valid := range regionsResponse.Regions.Region {
		validRegions = append(validRegions, valid.RegionId)
	}

	return validRegions, nil
}

func getConfigFromProfile(c *AlicloudAccessConfig, ProfileKey string) (interface{}, error) {
	providerConfig := make(map[string]interface{})
	current := c.AlicloudProfile
	if current != "" {
		profilePath, err := homedir.Expand(c.AlicloudSharedCredentialsFile)
		if err != nil {
			return nil, err
		}
		if profilePath == "" {
			profilePath = fmt.Sprintf("%s/.aliyun/config.json", os.Getenv("HOME"))
			if runtime.GOOS == "windows" {
				profilePath = fmt.Sprintf("%s/.aliyun/config.json", os.Getenv("USERPROFILE"))
			}
		}
		_, err = os.Stat(profilePath)
		if !os.IsNotExist(err) {
			data, err := ioutil.ReadFile(profilePath)
			if err != nil {
				return nil, err
			}
			config := map[string]interface{}{}
			err = json.Unmarshal(data, &config)
			if err != nil {
				return nil, err
			}
			for _, v := range config["profiles"].([]interface{}) {
				if current == v.(map[string]interface{})["name"] {
					providerConfig = v.(map[string]interface{})
				}
			}
		}
	}
	mode := ""
	if v, ok := providerConfig["mode"]; ok {
		mode = v.(string)
	} else {
		return v, nil
	}
	switch ProfileKey {
	case "access_key_id", "access_key_secret":
		if mode == "EcsRamRole" {
			return "", nil
		}
	case "ram_role_name":
		if mode != "EcsRamRole" {
			return "", nil
		}
	case "sts_token":
		if mode != "StsToken" {
			return "", nil
		}
	case "ram_role_arn", "ram_session_name":
		if mode != "RamRoleArn" {
			return "", nil
		}
	case "expired_seconds":
		if mode != "RamRoleArn" {
			return float64(0), nil
		}
	}
	return providerConfig[ProfileKey], nil
}
