package credentials

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

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

const defaultOIDCDurationSeconds = 3600

// OIDCCredential is a kind of credentials
type OIDCCredential struct {
	*credentialUpdater
	AccessKeyId           string
	AccessKeySecret       string
	RoleArn               string
	OIDCProviderArn       string
	OIDCTokenFilePath     string
	Policy                string
	RoleSessionName       string
	RoleSessionExpiration int
	sessionCredential     *sessionCredential
	runtime               *utils.Runtime
}

type OIDCResponse struct {
	Credentials *credentialsInResponse `json:"Credentials" xml:"Credentials"`
}

type OIDCcredentialsInResponse struct {
	AccessKeyId     string `json:"AccessKeyId" xml:"AccessKeyId"`
	AccessKeySecret string `json:"AccessKeySecret" xml:"AccessKeySecret"`
	SecurityToken   string `json:"SecurityToken" xml:"SecurityToken"`
	Expiration      string `json:"Expiration" xml:"Expiration"`
}

func newOIDCRoleArnCredential(accessKeyId, accessKeySecret, roleArn, OIDCProviderArn, OIDCTokenFilePath, RoleSessionName, policy string, RoleSessionExpiration int, runtime *utils.Runtime) *OIDCCredential {
	return &OIDCCredential{
		AccessKeyId:           accessKeyId,
		AccessKeySecret:       accessKeySecret,
		RoleArn:               roleArn,
		OIDCProviderArn:       OIDCProviderArn,
		OIDCTokenFilePath:     OIDCTokenFilePath,
		RoleSessionName:       RoleSessionName,
		Policy:                policy,
		RoleSessionExpiration: RoleSessionExpiration,
		credentialUpdater:     new(credentialUpdater),
		runtime:               runtime,
	}
}

func (e *OIDCCredential) GetCredential() (*CredentialModel, error) {
	if e.sessionCredential == nil || e.needUpdateCredential() {
		err := e.updateCredential()
		if err != nil {
			return nil, err
		}
	}
	credential := &CredentialModel{
		AccessKeyId:     tea.String(e.sessionCredential.AccessKeyId),
		AccessKeySecret: tea.String(e.sessionCredential.AccessKeySecret),
		SecurityToken:   tea.String(e.sessionCredential.SecurityToken),
		Type:            tea.String("oidc_role_arn"),
	}
	return credential, nil
}

// GetAccessKeyId reutrns OIDCCredential's AccessKeyId
// if AccessKeyId is not exist or out of date, the function will update it.
func (r *OIDCCredential) GetAccessKeyId() (*string, error) {
	if r.sessionCredential == nil || r.needUpdateCredential() {
		err := r.updateCredential()
		if err != nil {
			return tea.String(""), err
		}
	}
	return tea.String(r.sessionCredential.AccessKeyId), nil
}

// GetAccessSecret reutrns OIDCCredential's AccessKeySecret
// if AccessKeySecret is not exist or out of date, the function will update it.
func (r *OIDCCredential) GetAccessKeySecret() (*string, error) {
	if r.sessionCredential == nil || r.needUpdateCredential() {
		err := r.updateCredential()
		if err != nil {
			return tea.String(""), err
		}
	}
	return tea.String(r.sessionCredential.AccessKeySecret), nil
}

// GetSecurityToken reutrns OIDCCredential's SecurityToken
// if SecurityToken is not exist or out of date, the function will update it.
func (r *OIDCCredential) GetSecurityToken() (*string, error) {
	if r.sessionCredential == nil || r.needUpdateCredential() {
		err := r.updateCredential()
		if err != nil {
			return tea.String(""), err
		}
	}
	return tea.String(r.sessionCredential.SecurityToken), nil
}

// GetBearerToken is useless OIDCCredential
func (r *OIDCCredential) GetBearerToken() *string {
	return tea.String("")
}

// GetType reutrns OIDCCredential's type
func (r *OIDCCredential) GetType() *string {
	return tea.String("oidc_role_arn")
}

func (r *OIDCCredential) GetOIDCToken(OIDCTokenFilePath string) *string {
	tokenPath := OIDCTokenFilePath
	_, err := os.Stat(tokenPath)
	if os.IsNotExist(err) {
		tokenPath = os.Getenv("ALIBABA_CLOUD_OIDC_TOKEN_FILE")
		if tokenPath == "" {
			return nil
		}
	}
	byt, err := ioutil.ReadFile(tokenPath)
	if err != nil {
		return nil
	}
	return tea.String(string(byt))
}

func (r *OIDCCredential) updateCredential() (err error) {
	if r.runtime == nil {
		r.runtime = new(utils.Runtime)
	}
	request := request.NewCommonRequest()
	request.Domain = "sts.aliyuncs.com"
	if r.runtime.STSEndpoint != "" {
		request.Domain = r.runtime.STSEndpoint
	}
	request.Scheme = "HTTPS"
	request.Method = "POST"
	request.QueryParams["Timestamp"] = utils.GetTimeInFormatISO8601()
	request.QueryParams["Action"] = "AssumeRoleWithOIDC"
	request.QueryParams["Format"] = "JSON"
	request.BodyParams["RoleArn"] = r.RoleArn
	request.BodyParams["OIDCProviderArn"] = r.OIDCProviderArn
	token := r.GetOIDCToken(r.OIDCTokenFilePath)
	request.BodyParams["OIDCToken"] = tea.StringValue(token)
	if r.Policy != "" {
		request.QueryParams["Policy"] = r.Policy
	}
	if r.RoleSessionExpiration > 0 {
		request.QueryParams["DurationSeconds"] = strconv.Itoa(r.RoleSessionExpiration)
	}
	request.QueryParams["RoleSessionName"] = r.RoleSessionName
	request.QueryParams["Version"] = "2015-04-01"
	request.QueryParams["SignatureNonce"] = utils.GetUUID()
	request.Headers["Host"] = request.Domain
	request.Headers["Accept-Encoding"] = "identity"
	request.Headers["content-type"] = "application/x-www-form-urlencoded"
	request.URL = request.BuildURL()
	content, err := doAction(request, r.runtime)
	if err != nil {
		return fmt.Errorf("refresh RoleArn sts token err: %s", err.Error())
	}
	var resp *OIDCResponse
	err = json.Unmarshal(content, &resp)
	if err != nil {
		return fmt.Errorf("refresh RoleArn sts token err: Json.Unmarshal fail: %s", err.Error())
	}
	if resp == nil || resp.Credentials == nil {
		return fmt.Errorf("refresh RoleArn sts token err: Credentials is empty")
	}
	respCredentials := resp.Credentials
	if respCredentials.AccessKeyId == "" || respCredentials.AccessKeySecret == "" || respCredentials.SecurityToken == "" || respCredentials.Expiration == "" {
		return fmt.Errorf("refresh RoleArn sts token err: AccessKeyId: %s, AccessKeySecret: %s, SecurityToken: %s, Expiration: %s", respCredentials.AccessKeyId, respCredentials.AccessKeySecret, respCredentials.SecurityToken, respCredentials.Expiration)
	}

	expirationTime, err := time.Parse("2006-01-02T15:04:05Z", respCredentials.Expiration)
	r.lastUpdateTimestamp = time.Now().Unix()
	r.credentialExpiration = int(expirationTime.Unix() - time.Now().Unix())
	r.sessionCredential = &sessionCredential{
		AccessKeyId:     respCredentials.AccessKeyId,
		AccessKeySecret: respCredentials.AccessKeySecret,
		SecurityToken:   respCredentials.SecurityToken,
	}

	return
}
