package providers

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"time"

	httputil "github.com/aliyun/credentials-go/credentials/internal/http"
	"github.com/aliyun/credentials-go/credentials/internal/utils"
)

// OAuthTokenUpdateCallback 定义OAuth令牌更新回调函数类型
type OAuthTokenUpdateCallback func(refreshToken, accessToken, accessKey, secret, securityToken string, accessTokenExpire, stsExpire int64) error

type oauthCredentialResponse struct {
	AccessKeyId     string `json:"accessKeyId"`
	AccessKeySecret string `json:"accessKeySecret"`
	SecurityToken   string `json:"securityToken"`
	Expiration      string `json:"expiration"`
	RequestId       string `json:"requestId"`
}

type oauthRefreshTokenResponse struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	ExpiresIn    int64  `json:"expires_in"`
	TokenType    string `json:"token_type"`
}

type OAuthCredentialsProvider struct {
	clientId          string
	signInUrl         string
	refreshToken      string
	accessToken       string
	accessTokenExpire int64

	lastUpdateTimestamp int64
	expirationTimestamp int64
	sessionCredentials  *sessionCredentials
	// for http options
	httpOptions *HttpOptions
	// OAuth token call back
	tokenUpdateCallback OAuthTokenUpdateCallback
}

type OAuthCredentialsProviderBuilder struct {
	provider *OAuthCredentialsProvider
}

func NewOAuthCredentialsProviderBuilder() *OAuthCredentialsProviderBuilder {
	return &OAuthCredentialsProviderBuilder{
		provider: &OAuthCredentialsProvider{},
	}
}

func (b *OAuthCredentialsProviderBuilder) WithClientId(clientId string) *OAuthCredentialsProviderBuilder {
	b.provider.clientId = clientId
	return b
}

func (b *OAuthCredentialsProviderBuilder) WithSignInUrl(signInUrl string) *OAuthCredentialsProviderBuilder {
	b.provider.signInUrl = signInUrl
	return b
}

func (b *OAuthCredentialsProviderBuilder) WithRefreshToken(refreshToken string) *OAuthCredentialsProviderBuilder {
	b.provider.refreshToken = refreshToken
	return b
}

func (b *OAuthCredentialsProviderBuilder) WithAccessToken(accessToken string) *OAuthCredentialsProviderBuilder {
	b.provider.accessToken = accessToken
	return b
}

func (b *OAuthCredentialsProviderBuilder) WithAccessTokenExpire(accessTokenExpire int64) *OAuthCredentialsProviderBuilder {
	b.provider.accessTokenExpire = accessTokenExpire
	return b
}

func (b *OAuthCredentialsProviderBuilder) WithHttpOptions(httpOptions *HttpOptions) *OAuthCredentialsProviderBuilder {
	b.provider.httpOptions = httpOptions
	return b
}

func (b *OAuthCredentialsProviderBuilder) WithTokenUpdateCallback(callback OAuthTokenUpdateCallback) *OAuthCredentialsProviderBuilder {
	b.provider.tokenUpdateCallback = callback
	return b
}

func (b *OAuthCredentialsProviderBuilder) Build() (provider *OAuthCredentialsProvider, err error) {
	if b.provider.clientId == "" {
		err = errors.New("the ClientId is empty")
		return
	}

	if b.provider.signInUrl == "" {
		err = errors.New("the url for sign-in is empty")
		return
	}

	if b.provider.refreshToken == "" {
		err = errors.New("OAuth access token is empty or expired, please re-login with cli")
		return
	}

	provider = b.provider
	return
}

func (provider *OAuthCredentialsProvider) getCredentials() (session *sessionCredentials, err error) {

	if provider.accessToken == "" || provider.accessTokenExpire == 0 || provider.accessTokenExpire-time.Now().Unix() <= 180 {
		err = provider.tryRefreshOauthToken()
		if err != nil {
			return nil, err
		}
	}

	url, err := url.Parse(provider.signInUrl)
	if err != nil {
		return nil, err
	}

	req := &httputil.Request{
		Method:   "POST",
		Protocol: url.Scheme,
		Host:     url.Host,
		Path:     "/v1/exchange",
		Headers:  map[string]string{},
	}

	connectTimeout := 5 * time.Second
	readTimeout := 10 * time.Second

	if provider.httpOptions != nil && provider.httpOptions.ConnectTimeout > 0 {
		connectTimeout = time.Duration(provider.httpOptions.ConnectTimeout) * time.Millisecond
	}
	if provider.httpOptions != nil && provider.httpOptions.ReadTimeout > 0 {
		readTimeout = time.Duration(provider.httpOptions.ReadTimeout) * time.Millisecond
	}
	if provider.httpOptions != nil && provider.httpOptions.Proxy != "" {
		req.Proxy = provider.httpOptions.Proxy
	}
	req.ConnectTimeout = connectTimeout
	req.ReadTimeout = readTimeout

	// set headers
	req.Headers["Content-Type"] = "application/json"
	req.Headers["Authorization"] = fmt.Sprintf("Bearer %s", provider.accessToken)
	res, err := httpDo(req)
	if err != nil {
		return
	}

	if res.StatusCode != http.StatusOK {
		message := "get session token from OAuth failed: "
		err = errors.New(message + string(res.Body))
		return
	}
	var data oauthCredentialResponse
	err = json.Unmarshal(res.Body, &data)
	if err != nil {
		err = fmt.Errorf("get session token from OAuth failed, json.Unmarshal fail: %s", err.Error())
		return
	}

	if data.AccessKeyId == "" || data.AccessKeySecret == "" || data.SecurityToken == "" {
		err = fmt.Errorf("refresh session token err, fail to get credentials from OAuth: " + string(res.Body))
		return
	}

	session = &sessionCredentials{
		AccessKeyId:     data.AccessKeyId,
		AccessKeySecret: data.AccessKeySecret,
		SecurityToken:   data.SecurityToken,
		Expiration:      data.Expiration,
	}
	return
}

func (provider *OAuthCredentialsProvider) tryRefreshOauthToken() (err error) {
	refreshToken := provider.refreshToken
	clientId := provider.clientId

	url, err := url.Parse(provider.signInUrl)
	if err != nil {
		return
	}

	req := &httputil.Request{
		Method:   "POST",
		Protocol: url.Scheme,
		Host:     url.Host,
		Path:     "/v1/token",
		Headers:  map[string]string{},
	}

	connectTimeout := 5 * time.Second
	readTimeout := 10 * time.Second

	if provider.httpOptions != nil && provider.httpOptions.ConnectTimeout > 0 {
		connectTimeout = time.Duration(provider.httpOptions.ConnectTimeout) * time.Millisecond
	}
	if provider.httpOptions != nil && provider.httpOptions.ReadTimeout > 0 {
		readTimeout = time.Duration(provider.httpOptions.ReadTimeout) * time.Millisecond
	}
	if provider.httpOptions != nil && provider.httpOptions.Proxy != "" {
		req.Proxy = provider.httpOptions.Proxy
	}
	req.ConnectTimeout = connectTimeout
	req.ReadTimeout = readTimeout

	bodyForm := make(map[string]string)
	bodyForm["grant_type"] = "refresh_token"
	bodyForm["refresh_token"] = refreshToken
	bodyForm["client_id"] = clientId
	bodyForm["Timestamp"] = utils.GetTimeInFormatISO8601()
	req.Form = bodyForm

	req.Headers["Content-Type"] = "application/x-www-form-urlencoded"
	resp, err := httpDo(req)
	if err != nil {
		return
	}
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to refresh token, status code: %d", resp.StatusCode)
	}
	var tokenResp oauthRefreshTokenResponse
	err = json.Unmarshal(resp.Body, &tokenResp)
	if err != nil {
		err = fmt.Errorf("get refresh token from OAuth failed, json.Unmarshal fail: %s", err.Error())
		return
	}
	if tokenResp.RefreshToken == "" || tokenResp.AccessToken == "" {
		err = fmt.Errorf("failed to refresh token from OAuth: " + string(resp.Body))
		return
	}
	provider.accessToken = tokenResp.AccessToken
	provider.refreshToken = tokenResp.RefreshToken
	provider.accessTokenExpire = time.Now().Unix() + tokenResp.ExpiresIn

	return nil
}

func (provider *OAuthCredentialsProvider) needUpdateCredential() (result bool) {
	if provider.expirationTimestamp == 0 {
		return true
	}

	return provider.expirationTimestamp-time.Now().Unix() <= 180
}

func (provider *OAuthCredentialsProvider) GetCredentials() (cc *Credentials, err error) {
	if provider.sessionCredentials == nil || provider.needUpdateCredential() {
		sessionCredentials, err1 := provider.getCredentials()
		if err1 != nil {
			return nil, err1
		}

		provider.sessionCredentials = sessionCredentials
		expirationTime, err2 := time.Parse("2006-01-02T15:04:05Z", sessionCredentials.Expiration)
		if err2 != nil {
			return nil, err2
		}

		provider.lastUpdateTimestamp = time.Now().Unix()
		provider.expirationTimestamp = expirationTime.Unix()

		// 如果设置了回调函数，则调用回调函数写回配置文件
		if provider.tokenUpdateCallback != nil {
			err1 := provider.tokenUpdateCallback(provider.refreshToken, provider.accessToken, sessionCredentials.AccessKeyId, sessionCredentials.AccessKeySecret, sessionCredentials.SecurityToken, provider.accessTokenExpire, provider.expirationTimestamp)
			if err1 != nil {
				fmt.Printf("Warning: failed to update OAuth tokens in config file: %v\n", err)
			}
		}
	}

	cc = &Credentials{
		AccessKeyId:     provider.sessionCredentials.AccessKeyId,
		AccessKeySecret: provider.sessionCredentials.AccessKeySecret,
		SecurityToken:   provider.sessionCredentials.SecurityToken,
		ProviderName:    provider.GetProviderName(),
	}
	return
}

func (provider *OAuthCredentialsProvider) GetProviderName() string {
	return "oauth"
}
