package jdapi

import (
	"bytes"
	"crypto/md5"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"reflect"
	"sort"
	"strings"

	"git.rosy.net.cn/baseapi"

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

const (
	paramJson = "jd_param_json"
)

const (
	// ResponseCodeSuccess 操作成功
	ResponseCodeSuccess = "0"
	// ResponseCodeAccessFailed 操作失败，请重试，如还不能正常提供服务，可以提交工单（https://opengd.jd.com）详细说明一下，开放平台跟进回复。
	ResponseCodeAccessFailed = "-1"
	// ResponseCodeFailedCanAutoRetry 操作失败，系统会自动重试，如还不能正常提供服务，可以提交工单（https://opengd.jd.com）详细说明一下，开放平台跟进回复。
	ResponseCodeFailedCanAutoRetry = "-10000"
	ResponseCodeFailedAccessDB     = "10000"
	// ResponseCodeMissingMandatoryParam 请求参数中缺失必填项信息，请检查，如不清楚，请访问到家开放平台（https://opendj.jd.com）API分组有接口详细说明。
	ResponseCodeMissingMandatoryParam = "10005"
	ResponseCodeInvalidToken          = "10013"
	ResponseCodeInvalidSign           = "10014"
	ResponseCodeAbnormalParam         = "10015"
	ResponseCodeMethodNotExist        = "10018"
	ResponseCodeExceedLimit           = "10032"

	ResponseCodeTimeout        = "100022"
	ResponseCodeTomcateFailed  = "100023"
	ResponseCodeLoadUnexpected = "100024"
)

const (
	ResponseInnerCodeOrderAlreadyPickedUp = "10132"
	ResponseInnerCodeOrderCanceled        = "10138"
	ResponseInnerCodeOrderLocked          = "10139"

	ResponseInnerCodePartialFailed = "190005"
)

const (
	prodURL      = "https://openapi.jddj.com/djapi"
	signKey      = "sign"
	appSecretKey = "app_secret"

	AllPage         = 0
	DefaultPageSize = 50
)

const (
	JdVendorOrgCodeVagetable = "320406" // 菜市默认京东code
	JdVendorOrgCodeFruit     = "379599" // 水果默认京东code
	JdVendorOrgCodePet       = "390558" // 百货默认京东code
)

type API struct {
	platformapi.APICookie

	baseURL   string
	token     string
	appKey    string
	appSecret string
	client    *http.Client
	config    *platformapi.APIConfig
}

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

	canRetryCodes = map[string]int{
		ResponseCodeFailedCanAutoRetry: 1,
		ResponseCodeAccessFailed:       1,
		ResponseCodeFailedAccessDB:     1,
		ResponseCodeTimeout:            1,
		ResponseCodeTomcateFailed:      1,
		ResponseCodeLoadUnexpected:     1,
	}

	// innerCodeKeys = []string{"code", "retCode", "errorCode"}
	// innerCodeOKCodes = map[string]int{
	// 	"None":   1,
	// 	"0":      1,
	// 	"1":      1,
	// 	"200":    1,
	// 	"190005": 1, // 部分失败
	// }
	// noPageInnerDataKeys    = []string{"result", "data"}

	havePageInner2DataKeys = []string{"result", "resultList"}
	havePageTotalCountKeys = []string{"totalCount", "count"}
)

var (
	nullResultParser = genNoPageResultParser("code", "msg", "", "0")
	watchKeys        = []string{
		KeyOutStationNo,
		KeyStationNo,
		"StoreNo",
		KeyOutSkuId,
		KeySkuId,
	}
)

type PageResultParser func(map[string]interface{}, int) ([]interface{}, int, error)

// 京东签名
func (a *API) signParams(jdParams map[string]interface{}) string {
	var keys []string
	for k := range jdParams {
		if k != signKey {
			keys = append(keys, k)
		}
	}

	sort.Strings(keys)
	buf := &bytes.Buffer{}
	buf.WriteString(a.appSecret)
	for _, k := range keys {
		buf.WriteString(k)
		buf.WriteString(fmt.Sprint(jdParams[k]))
	}
	buf.WriteString(a.appSecret)
	return fmt.Sprintf("%X", md5.Sum(buf.Bytes()))
}

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

func NewFakeJD(token, baseURL string, config ...*platformapi.APIConfig) *API {
	a := New(token, "", "", config...)
	a.baseURL = baseURL
	return a
}

func NewPageOnly(cookie string, config ...*platformapi.APIConfig) *API {
	api := New("", "", "", config...)
	api.SetCookie(AccessStorePageCookieName, cookie)
	api.SetCookie(AccessStorePageCookieName2, cookie)
	return api
}

func (a *API) GetToken() (token string) {
	return a.token
}

func (a *API) GetAppKey() (appKey string) {
	return a.appKey
}

func (a *API) AccessAPI2(apiStr string, jdParams map[string]interface{}, traceInfo string) (retVal map[string]interface{}, err error) {
	params := make(map[string]interface{})
	params["v"] = "1.0"
	params["format"] = "json"
	params["app_key"] = a.appKey
	params["token"] = a.token

	if jdParams == nil {
		jdParams = make(map[string]interface{}, 0)
	}
	jdParamBytes, err := json.Marshal(jdParams)
	if err != nil {
		baseapi.SugarLogger.Errorf("Error when marshal %v, error:%v", jdParams, err)
		return nil, err
	}
	jdParamStr := string(jdParamBytes)
	userGet := true
	if true { //len(jdParamStr) > 12 {
		userGet = false
	}
	params["jd_param_json"] = jdParamStr
	err = platformapi.AccessPlatformAPIWithRetry(a.client,
		func() *http.Request {
			params["timestamp"] = utils.GetCurTimeStr()
			sign := a.signParams(params)
			params[signKey] = sign
			var request *http.Request
			if userGet {
				fullURL := utils.GenerateGetURL(a.baseURL, apiStr, params)
				request, _ = http.NewRequest(http.MethodGet, fullURL, nil)
			} else {
				fullURL := utils.GenerateGetURL(a.baseURL, apiStr, nil)
				request, _ = http.NewRequest(http.MethodPost, fullURL, strings.NewReader(utils.Map2URLValues(params).Encode()))
				request.Header.Set("charset", "UTF-8")
				request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
			}
			if traceInfo != "" {
				request.Header.Set(platformapi.KeyTrackInfo, traceInfo)
			}
			// 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")
			}
			code := utils.Interface2String(jsonResult1["code"])
			if code == "" {
				if jsonResult1[platformapi.KeyData] == nil {
					baseapi.SugarLogger.Warnf("abnormal jdapi apiStr:%s, jdParams:%s, result:%s", apiStr, utils.Format4Output(jdParams, true), utils.Format4Output(jsonResult1, true))
				} else {
					return platformapi.ErrLevelRecoverableErr, fmt.Errorf("%s return not json", apiStr)
				}
			}
			if code == ResponseCodeSuccess {
				retVal = jsonResult1
				return platformapi.ErrLevelSuccess, nil
			}
			newErr := utils.NewErrorCode(utils.Interface2String(jsonResult1["msg"]), 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, platformapi.RebuildError(err, jdParams, watchKeys)
}

func (a *API) AccessAPI(apiStr string, jdParams map[string]interface{}) (retVal map[string]interface{}, err error) {
	return a.AccessAPI2(apiStr, jdParams, "")
}

func genNoPageResultParser(codeKey, msgKey, resultKey, okCode string) func(data map[string]interface{}) (interface{}, error) {
	return func(data map[string]interface{}) (innerData interface{}, err error) {
		rawInnerCode, ok := data[codeKey]
		if !ok {
			panic(fmt.Sprintf("genNoPageResultParser codeKey %s can not be found in result:%v", codeKey, data))
		}
		innerCode := forceInnerCode2Str(rawInnerCode)
		if resultKey != "" {
			innerData, _ = data[resultKey]
		}
		if innerCode != okCode {
			errMsg := formatErrorMsg(data[msgKey])
			if innerCode == ResponseInnerCodePartialFailed {
				errMsg += ", " + utils.Format4Output(innerData, true)
			}
			err = utils.NewErrorCode(errMsg, innerCode, 1)
		}
		return innerData, err
	}
}

func formatErrorMsg(msg interface{}) (strMsg string) {
	if msg != nil {
		strMsg = utils.Format4Output(msg, true)
	}
	return strMsg
}

func (a *API) AccessAPINoPage2(apiStr string, jdParams map[string]interface{}, keyToRemove, keyToKeep []string, resultParser func(data map[string]interface{}) (interface{}, error), traceInfo string) (interface{}, error) {
	if resultParser == nil {
		resultParser = genNoPageResultParser("code", "msg", "result", "0")
	}
	jsonResult, err := a.AccessAPI2(apiStr, jdParams, traceInfo)
	if err != nil {
		return nil, err
	}
	if jsonResult["code"].(string) != "0" {
		return nil, errors.New(jsonResult["msg"].(string))
	}
	if (jsonResult["data"] == nil || utils.IsNil(jsonResult["data"])) && (jsonResult["encryptData"] != nil || utils.IsNil(jsonResult["encryptData"])) {
		decodeData, err := JDDecodeToData(a.appSecret, jsonResult["encryptData"].(string))
		if err != nil {
			return nil, err
		}
		jsonResult["data"] = string(decodeData)
	}
	var data map[string]interface{}
	if err := utils.UnmarshalUseNumber([]byte(jsonResult["data"].(string)), &data); err != nil {
		return nil, platformapi.ErrResponseDataFormatWrong
	}
	result, err := resultParser(data)
	if err == nil {
		return utils.DictKeysMan(result, keyToRemove, keyToKeep), nil
	}
	err = platformapi.RebuildError(err, jdParams, watchKeys)
	return result, err
}

func (a *API) AccessAPINoPage(apiStr string, jdParams map[string]interface{}, keyToRemove, keyToKeep []string, resultParser func(data map[string]interface{}) (interface{}, error)) (interface{}, error) {
	return a.AccessAPINoPage2(apiStr, jdParams, keyToRemove, keyToKeep, resultParser, "")
}
func genNormalHavePageResultParser(dataKey string) (handler PageResultParser) {
	return func(data map[string]interface{}, totalCount int) ([]interface{}, int, error) {
		var result map[string]interface{}
		var retVal []interface{}

		tempResult := data[dataKey]
		var result0 = make(map[string]interface{}, 0)
		if resultStr, ok := tempResult.(string); ok {
			if err := utils.UnmarshalUseNumber([]byte(resultStr), &result0); err != nil {
				return nil, 0, platformapi.ErrResponseDataFormatWrong
			}
		}
		if resultStr, ok := tempResult.(map[string]interface{}); ok {
			result0 = resultStr
		}

		result = result0
		if totalCount == 0 {
			for _, totalCountKey := range havePageTotalCountKeys {
				if totalCount2, ok := result[totalCountKey]; ok {
					totalCountInt64, _ := totalCount2.(json.Number).Int64()
					totalCount = int(totalCountInt64)
					if totalCount == 0 {
						return make([]interface{}, 0), 0, nil
					}
					break
				}
			}
			if totalCount == 0 {
				baseapi.SugarLogger.Errorf("can not find totalCount key, data:%v", result)
				return nil, 0, platformapi.ErrResponseDataFormatWrong
			}
		}

		for _, inner2ResultKey := range havePageInner2DataKeys {
			if inner2Result, ok := result[inner2ResultKey]; ok {
				if inner2Result == nil {
					retVal = nil
				} else if inner2ResultStr, ok := inner2Result.(string); ok {
					err := utils.UnmarshalUseNumber([]byte(inner2ResultStr), &retVal)
					if err != nil {
						return nil, 0, platformapi.ErrResponseDataFormatWrong
					}
				} else {
					retVal = inner2Result.([]interface{})
				}
				return retVal, totalCount, nil
			}
		}
		baseapi.SugarLogger.Errorf("can not find result key, data:%v", result)
		return nil, 0, platformapi.ErrResponseDataFormatWrong
	}
}

func (a *API) AccessAPIHavePage(apiStr string, jdParams map[string]interface{}, keyToRemove, keyToKeep []string, pageResultParser PageResultParser) ([]interface{}, int, error) {
	if pageResultParser == nil {
		pageResultParser = genNormalHavePageResultParser("result")
	}

	localJdParams := make(map[string]interface{})
	if jdParams != nil && len(jdParams) > 0 {
		for k, v := range jdParams {
			localJdParams[k] = v
		}
	}

	totalCount := 0
	pageNo := AllPage
	pageSize := DefaultPageSize
	if tempPageNo, ok := localJdParams[KeyPageNo]; ok {
		pageNo = tempPageNo.(int)
	}
	if tempPageSize, ok := localJdParams[KeyPageSize]; ok {
		pageSize = tempPageSize.(int)
	}

	curPage := pageNo
	if curPage == AllPage {
		curPage = 1
	}

	retVal := make([]interface{}, 0)
	for {
		localJdParams[KeyPageNo] = curPage
		localJdParams[KeyPageSize] = pageSize
		jsonResult, err := a.AccessAPI(apiStr, localJdParams)
		if err != nil {
			return nil, totalCount, err
		}
		var data map[string]interface{}
		if jsonResult["encryptData"] != nil {
			encryptData, err := JDDecodeToData(a.appSecret, jsonResult["encryptData"].(string))
			if err != nil {
				return nil, totalCount, platformapi.ErrResponseDataFormatWrong
			}
			if err := utils.UnmarshalUseNumber(encryptData, &data); err != nil {
				return nil, totalCount, platformapi.ErrResponseDataFormatWrong
			}
		} else if jsonResult["encryptData"] == nil && jsonResult["data"] != nil {
			if err := utils.UnmarshalUseNumber([]byte(jsonResult["data"].(string)), &data); err != nil {
				return nil, totalCount, platformapi.ErrResponseDataFormatWrong
			}
		} else {
			return nil, totalCount, platformapi.ErrResponseDataFormatWrong
		}

		innerCode := forceInnerCode2Str(data["code"])
		if innerCode != "0" {
			err2 := utils.NewErrorCode(getErrMsgFromData(data), innerCode, 1)
			baseapi.SugarLogger.Infof("AccessAPIHavePage failed, apiStr:%s, jdParams:%s, data:%s, error:%v", apiStr, utils.Format4Output(jdParams, true), utils.Format4Output(jsonResult, true), err2)
			return nil, totalCount, platformapi.RebuildError(err2, jdParams, watchKeys)
		}

		inResult, totalCount2, err := pageResultParser(data, totalCount)
		if err != nil {
			return nil, totalCount, platformapi.RebuildError(err, jdParams, watchKeys)
		}
		totalCount = totalCount2

		inResult = utils.DictKeysMan(inResult, keyToRemove, keyToKeep).([]interface{})
		retVal = append(retVal, inResult...)

		if curPage == pageNo || curPage*pageSize >= totalCount {
			break
		}
		curPage++
	}
	return retVal, totalCount, nil
}

func forceInnerCode2Str(innerCode interface{}) string {
	if innerCode == nil {
		return "0"
	} else if innerCodeStr, ok := innerCode.(string); ok {
		return innerCodeStr
	}
	return fmt.Sprintf("%v", innerCode)
}

func getErrMsgFromData(data map[string]interface{}) string {
	msg := utils.Interface2String(data["msg"])
	if msg == "" {
		msg = string(utils.MustMarshal(data))
		baseapi.SugarLogger.Warnf("getErrMsgFromData data:%v have no field msg!", data)
	}
	return msg
}

func JavaDateHook(fromType reflect.Type, toType reflect.Type, data interface{}) (interface{}, error) {
	if (fromType.Kind() >= reflect.Int && fromType.Kind() <= reflect.Uint64) ||
		fromType.Kind() == reflect.String {
		if toType.Kind() == reflect.Ptr {
			toType = toType.Elem()
		}
		if toType.Kind() == reflect.Struct &&
			toType.Name() == "JavaDate" {
			if milliSec := utils.Interface2Int64WithDefault(data, 0); milliSec > 0 {
				data = utils.NewJavaDateFromTime(utils.Timestamp2Time(milliSec))
			} else if fromType.Kind() == reflect.String {
				data = utils.NewJavaDateFromTime(utils.Str2Time(data.(string)))
			}
		}
	}
	return data, nil
}

func JdMap2StructByJson(inObj interface{}, outObjAddr interface{}, weaklyTypedInput bool) (err error) {
	return utils.Map2Struct(inObj, outObjAddr, true, "", JavaDateHook)
}

func JDDecodeToData(appSecret, data string) ([]byte, error) {
	key := appSecret[:16]
	iv := appSecret[16:32]
	sDec, err := base64.StdEncoding.DecodeString(data)
	if err != nil {
		return nil, err
	}
	resp, err := utils.AESCBCDecprytJD(sDec, []byte(key), []byte(iv))
	if err != nil {
		return nil, err
	}
	return resp, nil
}
