package dingdingapi

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

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

const (
	prodURL = "https://oapi.dingtalk.com"
)

const (
	getTokenAction    = "gettoken"
	snsGetTokenAction = "sns/gettoken"
)

const (
	ResponseCodeSuccess              = 0
	ResponseCodeCallbackAlreadyExist = 71006
	ResponseCodeCallbackNotExist     = 71007
	ResponseCodeCallbackURLNotExist  = 71012
)

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

	callbackToken  string
	callbackAESKey string
}

func New(corpID, appID, secret string, config ...*platformapi.APIConfig) *API {
	return NewWithAgentID(0, corpID, appID, secret, "", "", config...)
}

func NewWithAgentID(agentID int64, corpID, appID, secret, callbackToken, callbackAESKey string, config ...*platformapi.APIConfig) *API {
	curConfig := platformapi.DefAPIConfig
	if len(config) > 0 {
		curConfig = *config[0]
	}
	return &API{
		agentID:        agentID,
		corpID:         corpID,
		appID:          appID,
		secret:         secret,
		callbackToken:  callbackToken,
		callbackAESKey: callbackAESKey,
		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) SetToken(newToken string) bool {
	curToken := a.GetToken()
	if curToken != newToken {
		a.locker.Lock()
		defer a.locker.Unlock()
		a.token = newToken
		return true
	}
	return false
}

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

func (a *API) RetrieveToken() (token string, err error) {
	result, err := a.AccessAPI(getTokenAction, nil, nil)
	if err != nil {
		return "", err
	}
	token = utils.Interface2String(result["access_token"])
	a.SetToken(token)
	return token, nil
}

func (a *API) signParams(timestamp int64) string {
	mac := hmac.New(sha256.New, []byte(a.GetSecret()))
	mac.Write([]byte(utils.Int64ToStr(timestamp)))
	// return url.QueryEscape(base64.StdEncoding.EncodeToString(mac.Sum(nil)))
	return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}

func (a *API) AccessAPI(action string, params map[string]interface{}, bodyMap map[string]interface{}) (retVal map[string]interface{}, err error) {
	params2 := make(map[string]interface{})
	for k, v := range params {
		params2[k] = v
	}
	if action == getTokenAction {
		params2["appkey"] = a.GetAppID()
		params2["appsecret"] = a.GetSecret()
	} else if action == snsGetTokenAction {
		params2["appid"] = a.GetAppID()
		params2["appsecret"] = a.GetSecret()
	} else if !isSNSAction(action) {
		accessToken := a.GetToken()
		if accessToken == "" {
			panic("token is empty!")
		}
		params2["access_token"] = accessToken
	} else {
		params2["accessKey"] = a.GetAppID()
		timestamp := time.Now().UnixNano() / 1000000
		params2["timestamp"] = timestamp
		params2["signature"] = a.signParams(timestamp)
	}

	fullURL := utils.GenerateGetURL(prodURL, action, params2)
	// baseapi.SugarLogger.Debug(fullURL)

	err = platformapi.AccessPlatformAPIWithRetry(a.client,
		func() *http.Request {
			var request *http.Request
			if bodyMap == nil {
				request, _ = http.NewRequest(http.MethodGet, fullURL, nil)
			} else {
				request, _ = http.NewRequest(http.MethodPost, fullURL, bytes.NewReader(utils.MustMarshal(bodyMap)))
			}
			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")
			}
			errCode := int(utils.MustInterface2Int64(jsonResult1["errcode"]))
			if errCode == ResponseCodeSuccess {
				retVal = jsonResult1
				return platformapi.ErrLevelSuccess, nil
			}
			newErr := utils.NewErrorIntCode(utils.Interface2String(jsonResult1["errmsg"]), errCode)
			return platformapi.ErrLevelCodeIsNotOK, newErr
		})
	return retVal, err
}

func (a *API) GetJSAPITicket(getType string) (expiresIn int, ticket string, err error) {
	if getType == "" {
		getType = "jsapi"
	}
	result, err := a.AccessAPI("get_jsapi_ticket", map[string]interface{}{
		"type": getType,
	}, nil)
	if err != nil {
		return 0, "", err
	}
	return int(utils.MustInterface2Int64(result["expires_in"])), utils.Interface2String(result["ticket"]), nil
}
