package alipayapi

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"git.rosy.net.cn/baseapi"
	"git.rosy.net.cn/baseapi/platformapi"
	"git.rosy.net.cn/baseapi/utils"
	"io/ioutil"
	"net/http"
	"sort"
	"strings"
	"time"
)

const (
	prodURL = "https://openapi.alipay.com/gateway.do"

	signKey = "sign"
)

const (
	CommonErrSuccess             = "10000"
	CommonErrServiceNotAvailable = "20000"
)

type API struct {
	appID      string
	privateKey *rsa.PrivateKey
	client     *http.Client
	config     *platformapi.APIConfig
}

type KeyMobile struct {
	Code   string `json:"code"`
	Msg    string `json:"msg"`
	Mobile string `json:"mobile"`
}

var (
	commonCanRetryErrMap = map[string]bool{
		CommonErrServiceNotAvailable: true,
	}
)

func New(appID string, privateKey interface{}, config ...*platformapi.APIConfig) (a *API) {
	curConfig := platformapi.DefAPIConfig
	if len(config) > 0 {
		curConfig = *config[0]
	}
	var keyBytes []byte
	if keyBytes2, ok := privateKey.([]byte); ok {
		keyBytes = keyBytes2
	} else {
		keyBytes, _ = ioutil.ReadFile(privateKey.(string))
	}
	pubPem, _ := pem.Decode(keyBytes)
	if pubPem == nil {
		keyBytes, _ = base64.StdEncoding.DecodeString(string(keyBytes))
	} else {
		keyBytes = pubPem.Bytes
	}
	a = &API{
		appID:  appID,
		client: &http.Client{Timeout: curConfig.ClientTimeout},
		config: &curConfig,
	}
	if privateKey, err := x509.ParsePKCS1PrivateKey(keyBytes); err == nil {
		a.privateKey = privateKey
	} else {
		baseapi.SugarLogger.Errorf("alpayapi.New failed with err:%v", err)
	}
	return a
}

func (a *API) GetAppID() string {
	return a.appID
}

func (a *API) signParams(params map[string]interface{}) (sign string) {
	keys := make([]string, 0)
	for k := range params {
		if k != signKey {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)

	strList := make([]string, len(keys))
	for index, k := range keys {
		strList[index] = fmt.Sprintf("%s=%v", k, params[k])
	}

	finalStr := strings.Join(strList, "&")
	d := sha256.Sum256([]byte(finalStr))
	signature, _ := rsa.SignPKCS1v15(rand.Reader, a.privateKey, crypto.SHA256, d[:])
	return base64.StdEncoding.EncodeToString(signature)
}

func method2ResponseKey(method string) (responseKey string) {
	responseKey = strings.Join(append(strings.Split(method, "."), "response"), "_")
	return responseKey
}

func (a *API) AccessAPI(method string, params, bizContent map[string]interface{}, isPost bool) (retVal map[string]interface{}, err error) {
	params = utils.MergeMaps(map[string]interface{}{
		"app_id":    a.GetAppID(),
		"method":    method,
		"format":    "JSON",
		"charset":   "utf-8",
		"sign_type": "RSA2",
		"version":   "1.0",
	}, params)

	if len(bizContent) > 0 {
		params["biz_content"] = string(utils.MustMarshal(bizContent))
	}

	err = platformapi.AccessPlatformAPIWithRetry(a.client,
		func() *http.Request {
			var request *http.Request
			params["timestamp"] = utils.Time2Str(time.Now())
			params[signKey] = a.signParams(params)

			fullURL := utils.GenerateGetURL(prodURL, "", params)
			if isPost {
				request, _ = http.NewRequest(http.MethodPost, prodURL, strings.NewReader(utils.Map2URLValues(bizContent).Encode()))
			} else {
				request, _ = http.NewRequest(http.MethodGet, fullURL, 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")
			}
			if errorResponse, ok := jsonResult1["error_response"].(map[string]interface{}); ok {
				retVal = errorResponse
			} else {
				retVal, _ = jsonResult1[method2ResponseKey(method)].(map[string]interface{})
				if retVal == nil {
					return platformapi.ErrLevelGeneralFail, fmt.Errorf("结果格式异常")
				}
			}

			errCode := utils.Interface2String(retVal["code"])
			if errCode != CommonErrSuccess && errCode != "" {
				err = utils.NewErrorCode(utils.Interface2String(retVal["msg"]), errCode)
				if commonCanRetryErrMap[errCode] {
					return platformapi.ErrLevelRecoverableErr, err
				}
				errLevel = platformapi.ErrLevelGeneralFail
			}
			subErrCode := utils.Interface2String(retVal["sub_code"])
			if subErrCode != "" {
				return platformapi.ErrLevelCodeIsNotOK, utils.NewErrorCode(utils.Interface2String(retVal["sub_msg"]), subErrCode)
			} else if err != nil {
				return errLevel, err
			}

			return platformapi.ErrLevelSuccess, nil
		})
	return retVal, err
}
