package weixinapi

import (
	"bytes"
	"fmt"
	"math/rand"
	"mime/multipart"
	"net/http"
	"net/textproto"
	"strings"
	"sync"

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

const (
	prodURL = "https://api.weixin.qq.com"
)

const (
	ResponseCodeBusy    = -1
	ResponseCodeSuccess = 0

	letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)

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

type ErrorInfo struct {
	ErrCode int    `json:"errcode"`
	ErrMsg  string `json:"errmsg"`
}

type API struct {
	token  string
	appID  string
	secret string
	client *http.Client
	config *platformapi.APIConfig
	locker sync.RWMutex

	msgToken string
	msgKey   string
}

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

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

func (a *API) GetSecret() string {
	return a.secret
}

func isSNSAction(action string) bool {
	return strings.Index(action, "sns/") == 0
}

func (a *API) AccessAPI(action string, params map[string]interface{}, body string) (retVal map[string]interface{}, err error) {
	if params == nil && body == "" {
		panic("params and body can not all be empty")
	}
	params2 := make(map[string]interface{})
	for k, v := range params {
		params2[k] = v
	}

	if params2["grant_type"] != nil {
		params2["appid"] = a.appID
		params2["secret"] = a.secret
	} else if !isSNSAction(action) {
		accessToken := a.CBGetToken()
		if accessToken == "" {
			panic("token is empty!")
		}
		params2["access_token"] = accessToken
	}
	fullURL := utils.GenerateGetURL(prodURL, action, params2)

	err = platformapi.AccessPlatformAPIWithRetry(a.client,
		func() *http.Request {
			var request *http.Request
			if body == "" {
				request, _ = http.NewRequest(http.MethodGet, fullURL, nil)
			} else {
				request, _ = http.NewRequest(http.MethodPost, fullURL, strings.NewReader(body))
				if strings.Contains(fullURL, "wxa/generate_urllink") {
					request.Header.Set("Content-Type", "application/json")
				}
			}
			request.Close = true // todo try to fix EOF error when accessing weixin 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")
			}
			var errInfo *ErrorInfo
			// 微信的返回值，在错误与正常情况下，结构是完全不一样的
			if errCode, ok := jsonResult1["errcode"]; ok {
				errInfo = &ErrorInfo{
					ErrCode: int(utils.MustInterface2Int64(errCode)),
					ErrMsg:  jsonResult1["errmsg"].(string),
				}
				if errInfo.ErrCode == 0 {
					retVal = jsonResult1
				}
			} else {
				retVal = jsonResult1
			}
			if retVal != nil {
				return platformapi.ErrLevelSuccess, nil
			}
			newErr := utils.NewErrorIntCode(errInfo.ErrMsg, errInfo.ErrCode)
			if errInfo.ErrCode == ResponseCodeBusy {
				return platformapi.ErrLevelRecoverableErr, newErr
			}
			return platformapi.ErrLevelCodeIsNotOK, newErr
		})
	return retVal, err
}

func RandStringBytes(n int) string {
	b := make([]byte, n)
	for i := range b {
		b[i] = letterBytes[rand.Intn(len(letterBytes))]
	}
	return string(b)
}

func getBoundary() (boundary string) {
	boundary = "----WebKitFormBoundary"
	boundary += RandStringBytes(16)
	return boundary
}

func (a *API) AccessAPIUpload(url string, data []byte, fileName, contentType, materialType string) (retVal map[string]interface{}, err error) {
	params2 := make(map[string]interface{})
	accessToken := a.CBGetToken()
	if accessToken == "" {
		panic("token is empty!")
	}
	params2["access_token"] = accessToken
	fullURL := utils.GenerateGetURL(prodURL, url, params2)
	// baseapi.SugarLogger.Debug(fullURL)
	// 实例化multipart
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	writer.SetBoundary(getBoundary())
	// 创建multipart 文件字段
	h := make(textproto.MIMEHeader)
	h.Set("Content-Disposition",
		fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
			"media", fileName))
	h.Set("Content-Type", contentType)
	part, err := writer.CreatePart(h)
	// 写入文件数据到multipart，和读取本地文件方法的唯一区别
	_, err = part.Write(data)
	//将额外参数也写入到multipart
	if materialType != "" {
		_ = writer.WriteField("type", materialType)
	}
	err = writer.Close()
	err = platformapi.AccessPlatformAPIWithRetry(a.client,
		func() *http.Request {
			request, _ := http.NewRequest(http.MethodPost, fullURL, body)
			request.Header.Set("Content-Type", writer.FormDataContentType())
			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")
			}
			var errInfo *ErrorInfo
			// 微信的返回值，在错误与正常情况下，结构是完全不一样的
			if errCode, ok := jsonResult1["errcode"]; ok {
				errInfo = &ErrorInfo{
					ErrCode: int(utils.MustInterface2Int64(errCode)),
					ErrMsg:  jsonResult1["errmsg"].(string),
				}
				if errInfo.ErrCode == 0 {
					retVal = jsonResult1
				}
			} else {
				retVal = jsonResult1
			}
			if retVal != nil {
				return platformapi.ErrLevelSuccess, nil
			}
			newErr := utils.NewErrorIntCode(errInfo.ErrMsg, errInfo.ErrCode)
			if errInfo.ErrCode == ResponseCodeBusy {
				return platformapi.ErrLevelRecoverableErr, newErr
			}
			return platformapi.ErrLevelCodeIsNotOK, newErr
		})
	return retVal, err
}
