package weimobapi

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"sync"

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

const (
	ResponseCodeSuccess             = "0"
	ResponseCodeExceedCallFrequency = "8000103"
)

const (
	prodURL        = "https://dopen.weimob.com/api/1_0/ec"
	mediaURL       = "https://dopen.weimob.com/media/1_0/ec"
	authURL        = "https://dopen.weimob.com/fuwu/b"
	accessTokenAPI = "oauth2/token"
	uploadImgAPI   = "goodsImage/uploadImg"
)

const (
	// sku
	KeySkuID             = "skuId"
	KeyOuterSkuCode      = "outerSkuCode"
	KeyImageURL          = "imageUrl"
	KeySalePrice         = "salePrice"
	KeyOriginalPrice     = "originalPrice"
	KeyCostPrice         = "costPrice"
	KeyEditStockNum      = "editStockNum"
	KeyAvailableStockNum = "availableStockNum"
	KeySkuList           = "skuList"
	KeyB2cSku            = "b2cSku"
	KeySkuAttrMap        = "skuAttrMap"

	KeyField = "field"
	KeySort  = "sort"

	KeyGoodsClassifyId = "goodsClassifyId"
	KeySearch          = "search"
	KeyGoodsStatus     = "goodsStatus"
	KeyUpdateStartTime = "updateStartTime"
	KeyUpdateEndTime   = "updateEndTime"
)

type TokenInfo struct {
	TokenType             string `json:"token_type"`
	AccessToken           string `json:"access_token"`
	ExpiresIn             int    `json:"expires_in"`
	RefreshToken          string `json:"refresh_token"`
	RefreshTokenExpiresIn int    `json:"refresh_token_expires_in"`
	Scope                 string `json:"scope"`
	PublicAccountID       string `json:"public_account_id"`
	BusinessID            string `json:"business_id"`
}

type API struct {
	token     *TokenInfo
	appID     string
	appSecret string
	client    *http.Client
	config    *platformapi.APIConfig

	locker sync.RWMutex
}

var (
	exceedLimitCodes = map[string]int{
		ResponseCodeExceedCallFrequency: 1,
	}

	canRetryCodes = map[string]int{}
)

func New(token *TokenInfo, appID, appSecret string, config ...*platformapi.APIConfig) *API {
	curConfig := platformapi.DefAPIConfig
	if len(config) > 0 {
		curConfig = *config[0]
	}
	return &API{
		token:     token,
		appID:     appID,
		appSecret: appSecret,
		client:    &http.Client{Timeout: curConfig.ClientTimeout},
		config:    &curConfig,
	}
}

func (a *API) SetToken(newToken *TokenInfo) (retVal bool) {
	a.locker.Lock()
	defer a.locker.Unlock()
	a.token = newToken
	retVal = true
	return retVal
}

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

func (a *API) MustGetToken() *TokenInfo {
	token := a.GetToken()
	if token == nil {
		panic("token is nil")
	}
	return token
}

func (a *API) AccessAPI(apiStr string, apiParams map[string]interface{}) (retVal interface{}, err error) {
	err = platformapi.AccessPlatformAPIWithRetry(a.client,
		func() *http.Request {
			var request *http.Request
			if apiStr == accessTokenAPI {
				fullURL := utils.GenerateGetURL(authURL, apiStr, utils.MergeMaps(apiParams, map[string]interface{}{
					"client_id":     a.appID,
					"client_secret": a.appSecret,
				}))
				request, _ = http.NewRequest(http.MethodPost, fullURL, nil)
			} else {
				if apiStr == uploadImgAPI {
					fullURL := utils.GenerateGetURL(mediaURL, apiStr, utils.Params2Map("accesstoken", a.MustGetToken().AccessToken))
					var b bytes.Buffer
					w := multipart.NewWriter(&b)
					imgData := apiParams["file"].([]byte)
					imgName, _ := apiParams["name"].(string)
					if fw, err := w.CreateFormFile("file", imgName); err != nil {
						panic(err.Error())
					} else {
						fw.Write(imgData)
					}
					w.Close()
					// b.WriteString(utils.Map2URLValues(params).Encode())
					request, _ = http.NewRequest(http.MethodPost, fullURL, &b)
					request.Header.Set("Content-Type", w.FormDataContentType())
				} else {
					fullURL := utils.GenerateGetURL(prodURL, apiStr, utils.Params2Map("accesstoken", a.MustGetToken().AccessToken))
					var body io.Reader
					if apiParams != nil {
						apiParamsBytes, err := json.Marshal(apiParams)
						if err != nil {
							panic(fmt.Sprintf("Error when marshal %v, error:%v", apiParams, err))
						}
						body = bytes.NewReader(apiParamsBytes)
					}
					request, _ = http.NewRequest(http.MethodPost, fullURL, body)
					request.Header.Set("Content-Type", "application/json; charset=utf-8")
					request.Header.Set("Content-Encoding", "gzip, deflate")
					request.Header.Set("User-Agent", "weimob-golang-api")
					// request.Close = true //todo 为了性能考虑还是不要关闭
				}
			}
			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")
			}
			// baseapi.SugarLogger.Debug(utils.Format4Output(jsonResult1, false))
			code := ResponseCodeSuccess
			errMsg := ""
			if apiStr == accessTokenAPI {
				// token相关出错到不了这里，因为当取token发生错误时，HTTP CODE不是200
				if errCode, ok := jsonResult1["error"]; ok {
					code = getStringCode(errCode)
					errMsg = jsonResult1["error_description"].(string)
				}
				if code == ResponseCodeSuccess {
					retVal = jsonResult1
					return platformapi.ErrLevelSuccess, nil
				}
			} else {
				if errMap, ok := jsonResult1["code"].(map[string]interface{}); ok {
					code = getStringCode(errMap["errcode"])
					errMsg = errMap["errmsg"].(string)
				}
				if code == ResponseCodeSuccess {
					retVal = jsonResult1["data"]
					return platformapi.ErrLevelSuccess, nil
				}
			}
			newErr := utils.NewErrorCode(errMsg, code)
			if _, ok := exceedLimitCodes[code]; ok {
				return platformapi.ErrLevelExceedLimit, newErr
			} else if _, ok := canRetryCodes[code]; ok {
				return platformapi.ErrLevelRecoverableErr, newErr
			} else {
				return platformapi.ErrLevelCodeIsNotOK, newErr
			}
		})
	return retVal, err
}

func getStringCode(code interface{}) string {
	if codeStr, ok := code.(string); ok {
		return codeStr
	}
	return utils.Int64ToStr(utils.Interface2Int64WithDefault(code, 0))
}

// redirectURL只是作为校验，要求转入得到此CODE时给的redirectURL，不包括参数部分
func (a *API) RefreshTokenByCode(code, redirectURL string) (retVal *TokenInfo, err error) {
	result, err := a.AccessAPI(accessTokenAPI, map[string]interface{}{
		"grant_type":   "authorization_code",
		"code":         code,
		"redirect_uri": redirectURL,
	})
	if err == nil {
		retVal = map2TokenInfo(result.(map[string]interface{}))
		a.SetToken(retVal)
		return retVal, nil
	}
	return nil, err
}

func (a *API) RefreshTokenByRefreshToken() (retVal *TokenInfo, err error) {
	curToken := a.GetToken()
	if curToken != nil {
		result, err := a.AccessAPI(accessTokenAPI, map[string]interface{}{
			"grant_type":    "refresh_token",
			"refresh_token": curToken.RefreshToken,
		})
		if err == nil {
			retVal = map2TokenInfo(result.(map[string]interface{}))
			a.SetToken(retVal)
			return retVal, nil

		}
		return nil, err
	}
	return nil, fmt.Errorf("刷新TOKEN要求已经有TOKEN")
}

func map2TokenInfo(mapData map[string]interface{}) *TokenInfo {
	return &TokenInfo{
		TokenType:             utils.Interface2String(mapData["token_type"]),
		AccessToken:           utils.Interface2String(mapData["access_token"]),
		ExpiresIn:             int(utils.MustInterface2Int64(mapData["expires_in"])),
		RefreshToken:          utils.Interface2String(mapData["refresh_token"]),
		RefreshTokenExpiresIn: int(utils.MustInterface2Int64(mapData["refresh_token_expires_in"])),
		Scope:                 utils.Interface2String(mapData["scope"]),
		PublicAccountID:       utils.Interface2String(mapData["public_account_id"]),
		BusinessID:            utils.Interface2String(mapData["business_id"]),
	}
}
