package credentials

import (
	"bufio"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"github.com/alibabacloud-go/debug/debug"
	"github.com/alibabacloud-go/tea/tea"
	"github.com/aliyun/credentials-go/credentials/request"
	"github.com/aliyun/credentials-go/credentials/response"
	"github.com/aliyun/credentials-go/credentials/utils"
)

var debuglog = debug.Init("credential")

var hookParse = func(err error) error {
	return err
}

// Credential is an interface for getting actual credential
type Credential interface {
	GetAccessKeyId() (*string, error)
	GetAccessKeySecret() (*string, error)
	GetSecurityToken() (*string, error)
	GetBearerToken() *string
	GetType() *string
	GetCredential() (*CredentialModel, error)
}

// Config is important when call NewCredential
type Config struct {
	Type                  *string  `json:"type"`
	AccessKeyId           *string  `json:"access_key_id"`
	AccessKeySecret       *string  `json:"access_key_secret"`
	OIDCProviderArn       *string  `json:"oidc_provider_arn"`
	OIDCTokenFilePath     *string  `json:"oidc_token"`
	RoleArn               *string  `json:"role_arn"`
	RoleSessionName       *string  `json:"role_session_name"`
	PublicKeyId           *string  `json:"public_key_id"`
	RoleName              *string  `json:"role_name"`
	SessionExpiration     *int     `json:"session_expiration"`
	PrivateKeyFile        *string  `json:"private_key_file"`
	BearerToken           *string  `json:"bearer_token"`
	SecurityToken         *string  `json:"security_token"`
	RoleSessionExpiration *int     `json:"role_session_expiratioon"`
	Policy                *string  `json:"policy"`
	Host                  *string  `json:"host"`
	Timeout               *int     `json:"timeout"`
	ConnectTimeout        *int     `json:"connect_timeout"`
	Proxy                 *string  `json:"proxy"`
	InAdvanceScale        *float64 `json:"inAdvanceScale"`
	Url                   *string  `json:"url"`
	STSEndpoint           *string  `json:"sts_endpoint"`
	ExternalId            *string  `json:"external_id"`
}

func (s Config) String() string {
	return tea.Prettify(s)
}

func (s Config) GoString() string {
	return s.String()
}

func (s *Config) SetAccessKeyId(v string) *Config {
	s.AccessKeyId = &v
	return s
}

func (s *Config) SetAccessKeySecret(v string) *Config {
	s.AccessKeySecret = &v
	return s
}

func (s *Config) SetSecurityToken(v string) *Config {
	s.SecurityToken = &v
	return s
}

func (s *Config) SetRoleArn(v string) *Config {
	s.RoleArn = &v
	return s
}

func (s *Config) SetRoleSessionName(v string) *Config {
	s.RoleSessionName = &v
	return s
}

func (s *Config) SetPublicKeyId(v string) *Config {
	s.PublicKeyId = &v
	return s
}

func (s *Config) SetRoleName(v string) *Config {
	s.RoleName = &v
	return s
}

func (s *Config) SetSessionExpiration(v int) *Config {
	s.SessionExpiration = &v
	return s
}

func (s *Config) SetPrivateKeyFile(v string) *Config {
	s.PrivateKeyFile = &v
	return s
}

func (s *Config) SetBearerToken(v string) *Config {
	s.BearerToken = &v
	return s
}

func (s *Config) SetRoleSessionExpiration(v int) *Config {
	s.RoleSessionExpiration = &v
	return s
}

func (s *Config) SetPolicy(v string) *Config {
	s.Policy = &v
	return s
}

func (s *Config) SetHost(v string) *Config {
	s.Host = &v
	return s
}

func (s *Config) SetTimeout(v int) *Config {
	s.Timeout = &v
	return s
}

func (s *Config) SetConnectTimeout(v int) *Config {
	s.ConnectTimeout = &v
	return s
}

func (s *Config) SetProxy(v string) *Config {
	s.Proxy = &v
	return s
}

func (s *Config) SetType(v string) *Config {
	s.Type = &v
	return s
}

func (s *Config) SetOIDCTokenFilePath(v string) *Config {
	s.OIDCTokenFilePath = &v
	return s
}

func (s *Config) SetOIDCProviderArn(v string) *Config {
	s.OIDCProviderArn = &v
	return s
}

func (s *Config) SetURLCredential(v string) *Config {
	if v == "" {
		v = os.Getenv("ALIBABA_CLOUD_CREDENTIALS_URI")
	}
	s.Url = &v
	return s
}

func (s *Config) SetSTSEndpoint(v string) *Config {
	s.STSEndpoint = &v
	return s
}

// NewCredential return a credential according to the type in config.
// if config is nil, the function will use default provider chain to get credential.
// please see README.md for detail.
func NewCredential(config *Config) (credential Credential, err error) {
	if config == nil {
		config, err = defaultChain.resolve()
		if err != nil {
			return
		}
		return NewCredential(config)
	}
	switch tea.StringValue(config.Type) {
	case "credentials_uri":
		credential = newURLCredential(tea.StringValue(config.Url))
	case "oidc_role_arn":
		err = checkoutAssumeRamoidc(config)
		if err != nil {
			return
		}
		runtime := &utils.Runtime{
			Host:           tea.StringValue(config.Host),
			Proxy:          tea.StringValue(config.Proxy),
			ReadTimeout:    tea.IntValue(config.Timeout),
			ConnectTimeout: tea.IntValue(config.ConnectTimeout),
			STSEndpoint:    tea.StringValue(config.STSEndpoint),
		}
		credential = newOIDCRoleArnCredential(tea.StringValue(config.AccessKeyId), tea.StringValue(config.AccessKeySecret), tea.StringValue(config.RoleArn), tea.StringValue(config.OIDCProviderArn), tea.StringValue(config.OIDCTokenFilePath), tea.StringValue(config.RoleSessionName), tea.StringValue(config.Policy), tea.IntValue(config.RoleSessionExpiration), runtime)
	case "access_key":
		err = checkAccessKey(config)
		if err != nil {
			return
		}
		credential = newAccessKeyCredential(tea.StringValue(config.AccessKeyId), tea.StringValue(config.AccessKeySecret))
	case "sts":
		err = checkSTS(config)
		if err != nil {
			return
		}
		credential = newStsTokenCredential(tea.StringValue(config.AccessKeyId), tea.StringValue(config.AccessKeySecret), tea.StringValue(config.SecurityToken))
	case "ecs_ram_role":
		checkEcsRAMRole(config)
		runtime := &utils.Runtime{
			Host:           tea.StringValue(config.Host),
			Proxy:          tea.StringValue(config.Proxy),
			ReadTimeout:    tea.IntValue(config.Timeout),
			ConnectTimeout: tea.IntValue(config.ConnectTimeout),
		}
		credential = newEcsRAMRoleCredential(tea.StringValue(config.RoleName), tea.Float64Value(config.InAdvanceScale), runtime)
	case "ram_role_arn":
		err = checkRAMRoleArn(config)
		if err != nil {
			return
		}
		runtime := &utils.Runtime{
			Host:           tea.StringValue(config.Host),
			Proxy:          tea.StringValue(config.Proxy),
			ReadTimeout:    tea.IntValue(config.Timeout),
			ConnectTimeout: tea.IntValue(config.ConnectTimeout),
			STSEndpoint:    tea.StringValue(config.STSEndpoint),
		}
		credential = newRAMRoleArnWithExternalIdCredential(
			tea.StringValue(config.AccessKeyId),
			tea.StringValue(config.AccessKeySecret),
			tea.StringValue(config.RoleArn),
			tea.StringValue(config.RoleSessionName),
			tea.StringValue(config.Policy),
			tea.IntValue(config.RoleSessionExpiration),
			tea.StringValue(config.ExternalId),
			runtime)
	case "rsa_key_pair":
		err = checkRSAKeyPair(config)
		if err != nil {
			return
		}
		file, err1 := os.Open(tea.StringValue(config.PrivateKeyFile))
		if err1 != nil {
			err = fmt.Errorf("InvalidPath: Can not open PrivateKeyFile, err is %s", err1.Error())
			return
		}
		defer file.Close()
		var privateKey string
		scan := bufio.NewScanner(file)
		for scan.Scan() {
			if strings.HasPrefix(scan.Text(), "----") {
				continue
			}
			privateKey += scan.Text() + "\n"
		}
		runtime := &utils.Runtime{
			Host:           tea.StringValue(config.Host),
			Proxy:          tea.StringValue(config.Proxy),
			ReadTimeout:    tea.IntValue(config.Timeout),
			ConnectTimeout: tea.IntValue(config.ConnectTimeout),
			STSEndpoint:    tea.StringValue(config.STSEndpoint),
		}
		credential = newRsaKeyPairCredential(privateKey, tea.StringValue(config.PublicKeyId), tea.IntValue(config.SessionExpiration), runtime)
	case "bearer":
		if tea.StringValue(config.BearerToken) == "" {
			err = errors.New("BearerToken cannot be empty")
			return
		}
		credential = newBearerTokenCredential(tea.StringValue(config.BearerToken))
	default:
		err = errors.New("Invalid type option, support: access_key, sts, ecs_ram_role, ram_role_arn, rsa_key_pair")
		return
	}
	return credential, nil
}

func checkRSAKeyPair(config *Config) (err error) {
	if tea.StringValue(config.PrivateKeyFile) == "" {
		err = errors.New("PrivateKeyFile cannot be empty")
		return
	}
	if tea.StringValue(config.PublicKeyId) == "" {
		err = errors.New("PublicKeyId cannot be empty")
		return
	}
	return
}

func checkoutAssumeRamoidc(config *Config) (err error) {
	if tea.StringValue(config.RoleArn) == "" {
		err = errors.New("RoleArn cannot be empty")
		return
	}
	if tea.StringValue(config.OIDCProviderArn) == "" {
		err = errors.New("OIDCProviderArn cannot be empty")
		return
	}
	return
}

func checkRAMRoleArn(config *Config) (err error) {
	if tea.StringValue(config.AccessKeySecret) == "" {
		err = errors.New("AccessKeySecret cannot be empty")
		return
	}
	if tea.StringValue(config.RoleArn) == "" {
		err = errors.New("RoleArn cannot be empty")
		return
	}
	if tea.StringValue(config.RoleSessionName) == "" {
		err = errors.New("RoleSessionName cannot be empty")
		return
	}
	if tea.StringValue(config.AccessKeyId) == "" {
		err = errors.New("AccessKeyId cannot be empty")
		return
	}
	return
}

func checkEcsRAMRole(config *Config) (err error) {
	return
}

func checkSTS(config *Config) (err error) {
	if tea.StringValue(config.AccessKeyId) == "" {
		err = errors.New("AccessKeyId cannot be empty")
		return
	}
	if tea.StringValue(config.AccessKeySecret) == "" {
		err = errors.New("AccessKeySecret cannot be empty")
		return
	}
	if tea.StringValue(config.SecurityToken) == "" {
		err = errors.New("SecurityToken cannot be empty")
		return
	}
	return
}

func checkAccessKey(config *Config) (err error) {
	if tea.StringValue(config.AccessKeyId) == "" {
		err = errors.New("AccessKeyId cannot be empty")
		return
	}
	if tea.StringValue(config.AccessKeySecret) == "" {
		err = errors.New("AccessKeySecret cannot be empty")
		return
	}
	return
}

func doAction(request *request.CommonRequest, runtime *utils.Runtime) (content []byte, err error) {
	var urlEncoded string
	if request.BodyParams != nil {
		urlEncoded = utils.GetURLFormedMap(request.BodyParams)
	}
	httpRequest, err := http.NewRequest(request.Method, request.URL, strings.NewReader(urlEncoded))
	if err != nil {
		return
	}
	httpRequest.Proto = "HTTP/1.1"
	httpRequest.Host = request.Domain
	debuglog("> %s %s %s", httpRequest.Method, httpRequest.URL.RequestURI(), httpRequest.Proto)
	debuglog("> Host: %s", httpRequest.Host)
	for key, value := range request.Headers {
		if value != "" {
			debuglog("> %s: %s", key, value)
			httpRequest.Header[key] = []string{value}
		}
	}
	debuglog(">")
	httpClient := &http.Client{}
	httpClient.Timeout = time.Duration(runtime.ReadTimeout) * time.Second
	proxy := &url.URL{}
	if runtime.Proxy != "" {
		proxy, err = url.Parse(runtime.Proxy)
		if err != nil {
			return
		}
	}
	trans := &http.Transport{}
	if proxy != nil && runtime.Proxy != "" {
		trans.Proxy = http.ProxyURL(proxy)
	}
	trans.DialContext = utils.Timeout(time.Duration(runtime.ConnectTimeout) * time.Second)
	httpClient.Transport = trans
	httpResponse, err := hookDo(httpClient.Do)(httpRequest)
	if err != nil {
		return
	}
	debuglog("< %s %s", httpResponse.Proto, httpResponse.Status)
	for key, value := range httpResponse.Header {
		debuglog("< %s: %v", key, strings.Join(value, ""))
	}
	debuglog("<")

	resp := &response.CommonResponse{}
	err = hookParse(resp.ParseFromHTTPResponse(httpResponse))
	if err != nil {
		return
	}
	debuglog("%s", resp.GetHTTPContentString())
	if resp.GetHTTPStatus() != http.StatusOK {
		err = fmt.Errorf("httpStatus: %d, message = %s", resp.GetHTTPStatus(), resp.GetHTTPContentString())
		return
	}
	return resp.GetHTTPContentBytes(), nil
}
