package provider

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"time"

	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth"
	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
)

var securityCredURL = "http://100.100.100.200/latest/meta-data/ram/security-credentials/"

type InstanceCredentialsProvider struct{}

var ProviderInstance = new(InstanceCredentialsProvider)

var HookGet = func(fn func(string) (int, []byte, error)) func(string) (int, []byte, error) {
	return fn
}

func NewInstanceCredentialsProvider() Provider {
	return &InstanceCredentialsProvider{}
}

func (p *InstanceCredentialsProvider) Resolve() (auth.Credential, error) {
	roleName, ok := os.LookupEnv(ENVEcsMetadata)
	if !ok {
		return nil, nil
	}
	if roleName == "" {
		return nil, errors.New("environmental variable 'ALIBABA_CLOUD_ECS_METADATA' are empty")
	}
	status, content, err := HookGet(get)(securityCredURL + roleName)
	if err != nil {
		return nil, err
	}
	if status != 200 {
		if status == 404 {
			return nil, errors.New("the role was not found in the instance")
		}
		return nil, fmt.Errorf("received %d when getting security credentials for %s", status, roleName)
	}
	body := make(map[string]interface{})

	if err := json.Unmarshal(content, &body); err != nil {
		return nil, err
	}

	accessKeyID, err := extractString(body, "AccessKeyId")
	if err != nil {
		return nil, err
	}
	accessKeySecret, err := extractString(body, "AccessKeySecret")
	if err != nil {
		return nil, err
	}
	securityToken, err := extractString(body, "SecurityToken")
	if err != nil {
		return nil, err
	}

	return credentials.NewStsTokenCredential(accessKeyID, accessKeySecret, securityToken), nil
}

func get(url string) (status int, content []byte, err error) {
	httpClient := &http.Client{
		Timeout: 1 * time.Second,
	}
	resp, err := httpClient.Get(url)
	if err != nil {
		return
	}
	defer resp.Body.Close()
	content, err = ioutil.ReadAll(resp.Body)
	return resp.StatusCode, content, err
}

func extractString(m map[string]interface{}, key string) (string, error) {
	raw, ok := m[key]
	if !ok {
		return "", fmt.Errorf("%s not in map", key)
	}
	str, ok := raw.(string)
	if !ok {
		return "", fmt.Errorf("%s is not a string in map", key)
	}
	return str, nil
}
