package elmapi

import (
	"bytes"
	"crypto/md5"
	"encoding/base64"
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strings"
	"sync"

	"git.rosy.net.cn/baseapi/platformapi"
	"git.rosy.net.cn/baseapi/utils"
)

const (
	sandboxURL          = "https://open-api-sandbox.shop.ele.me/api/v1"
	sandboxAuthorizeURL = "https://open-api-sandbox.shop.ele.me/authorize"
	snadboxTokenURL     = "https://open-api-sandbox.shop.ele.me/token"

	prodURL          = "https://open-api.shop.ele.me/api/v1"
	prodAuthorizeURL = "https://open-api.shop.ele.me/authorize"
	prodTokenURL     = "https://open-api.shop.ele.me/token"

	signKey = "signature"
)

type ResponseResult struct {
	ID     string
	Result interface{}
	Error  map[string]interface{}
}

type TokenInfo struct {
	AccessToken string `json:"access_token"`
	TokenType   string `json:"token_type"`
	ExpiresIn   int    `json:"expires_in"`
}

type API struct {
	token   string
	appKey  string
	secret  string
	isProd  bool
	fullURL string
	client  *http.Client
	config  *platformapi.APIConfig
	locker  sync.RWMutex
}

type payload struct {
	Token     string                 `json:"token"`
	Nop       string                 `json:"nop"`
	Metas     map[string]interface{} `json:"metas"`
	Params    map[string]interface{} `json:"params"`
	Action    string                 `json:"action"`
	ID        string                 `json:"id"`
	Signature string                 `json:"signature"`
}

func New(token, appKey, secret string, isProd bool, config ...*platformapi.APIConfig) *API {
	curConfig := platformapi.DefAPIConfig
	if len(config) > 0 {
		curConfig = *config[0]
	}
	api := &API{
		token:  token,
		appKey: appKey,
		secret: secret,
		isProd: isProd,
		client: &http.Client{Timeout: curConfig.ClientTimeout},
		config: &curConfig,
	}

	if isProd {
		api.fullURL = prodURL + "/"
	} else {
		api.fullURL = sandboxURL + "/"
	}
	return api
}

func (a *API) SetToken(newToken string) (retVal bool) {
	curToken := a.GetToken()
	if curToken != newToken {
		a.locker.Lock()
		defer a.locker.Unlock()
		a.token = newToken
		retVal = true
		// baseapi.SugarLogger.Debugf("elm token changed from %v to %v", curToken, newToken)
	}
	return retVal
}

func (a *API) GetToken() string {
	a.locker.RLock()
	defer a.locker.RUnlock()
	return a.token
}

func (a *API) signParamsMap(mapData map[string]interface{}, prefix string) string {
	keyValues := make([]string, 0)
	for k, v := range mapData {
		if k != signKey {
			vStr := ""
			if prefix == "" { // callback sign
				vStr = fmt.Sprint(v)
			} else { // call sign
				vBytes := utils.MustMarshal(v)
				vStr = string(vBytes)
			}
			keyValues = append(keyValues, k+"="+vStr)
		}
	}

	sort.Strings(keyValues)
	finalStr := prefix + strings.Join(keyValues, "") + a.secret
	return fmt.Sprintf("%X", md5.Sum([]byte(finalStr)))
}

func (a *API) signParams(action string, pl *payload) string {
	mapData := utils.MergeMaps(pl.Metas, pl.Params)
	return a.signParamsMap(mapData, action+a.GetToken())
}

func (a *API) AccessAPI(action string, params map[string]interface{}) (retVal *ResponseResult, err error) {
	if params == nil {
		params = make(map[string]interface{}, 0)
	}
	metas := map[string]interface{}{
		"app_key":   a.appKey,
		"timestamp": utils.GetCurTimestamp(),
	}

	pl := &payload{
		Token:  a.GetToken(),
		Nop:    "1.0.0",
		Metas:  metas,
		Params: params,
		Action: action,
		ID:     utils.GetUUID(),
	}

	pl.Signature = a.signParams(action, pl)
	plBytes := utils.MustMarshal(pl)

	err = platformapi.AccessPlatformAPIWithRetry(a.client,
		func() *http.Request {
			request, _ := http.NewRequest(http.MethodPost, a.fullURL, bytes.NewReader(plBytes))
			request.Header.Set("Content-Type", "application/json; charset=utf-8")
			request.Header.Set("Content-Encoding", "gzip, deflate")
			request.Header.Set("User-Agent", "eleme-golang-api")
			return request
		},
		a.config,
		func(response *http.Response, bodyStr string, jsonResult1 map[string]interface{}) (errLevel string, err error) {
			if jsonResult1 == nil {
				return platformapi.ErrLevelRecoverableErr, fmt.Errorf("mapData is nil")
			}
			resultError, _ := jsonResult1["error"].(map[string]interface{})
			retVal = &ResponseResult{
				ID:     jsonResult1["id"].(string),
				Error:  resultError,
				Result: jsonResult1["result"],
			}
			errinfoMap := retVal.Error
			if errinfoMap == nil {
				return platformapi.ErrLevelSuccess, nil
			}
			code := errinfoMap["code"].(string)
			newErr := utils.NewErrorCode(errinfoMap["message"].(string), code)
			if code == "EXCEED_LIMIT" {
				return platformapi.ErrLevelExceedLimit, newErr
			} else if code == "SERVER_ERROR" || code == "BIZ_SYSTEM_ERROR" || code == "BIZ_1006" || code == "BUSINESS_ERROR" {
				return platformapi.ErrLevelRecoverableErr, newErr
			} else {
				return platformapi.ErrLevelCodeIsNotOK, newErr
			}
		})
	return retVal, err
}

func (a *API) getTokenURL() string {
	if a.isProd {
		return prodTokenURL
	}
	return snadboxTokenURL
}

func (a *API) getAuthorizeURL() string {
	if a.isProd {
		return prodAuthorizeURL
	}
	return sandboxAuthorizeURL
}

func (a *API) AcccessAPI2(baseURL string, params map[string]interface{}, method string) (retVal map[string]interface{}, err error) {
	err = platformapi.AccessPlatformAPIWithRetry(a.client,
		func() *http.Request {
			var request *http.Request
			if method == http.MethodPost {
				params2 := make(url.Values)
				for k, v := range params {
					params2[k] = []string{fmt.Sprint(v)}
				}
				request, _ = http.NewRequest(method, baseURL, strings.NewReader(params2.Encode()))
				request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
				request.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(a.appKey+":"+a.secret)))
			} else {
				request, _ = http.NewRequest(method, utils.GenerateGetURL(baseURL, "", params), nil)
			}
			return request
		},
		a.config,
		func(response *http.Response, bodyStr string, jsonResult1 map[string]interface{}) (errLevel string, err error) {
			if jsonResult1 == nil {
				return platformapi.ErrLevelRecoverableErr, fmt.Errorf("mapData is nil")
			}
			retVal = jsonResult1
			return platformapi.ErrLevelSuccess, nil
		})

	return retVal, err
}

func (a *API) RefreshTokenIndividual() (retVal *TokenInfo, err error) {
	result, err := a.AcccessAPI2(a.getTokenURL(), utils.Params2Map("grant_type", "client_credentials", "scope", "all"), http.MethodPost)
	if err != nil {
		return nil, err
	}
	retVal = &TokenInfo{}

	if accessToken, ok := result["access_token"]; ok {
		retVal.AccessToken = accessToken.(string)
		retVal.TokenType = result["token_type"].(string)
		retVal.ExpiresIn = int(utils.MustInterface2Int64(result["expires_in"]))

		// update my token too.
		a.SetToken(retVal.AccessToken)
		return retVal, nil
	}

	return nil, platformapi.ErrResponseDataFormatWrong
}
