package wechat

import (
	"crypto/sha1"
	"encoding/hex"
	"fmt"
	"net/url"
	"sort"
	"strings"
	"time"

	"quyun/pkg/oauth"

	"github.com/imroc/req/v3"
	"github.com/pkg/errors"
)

const BaseURL = "https://api.weixin.qq.com/"

var DefaultClient = req.
	NewClient().
	SetBaseURL(BaseURL).
	SetCommonHeader("Content-Type", "application/json")

const (
	ScopeBase     = "snsapi_base"
	ScopeUserInfo = "snsapi_userinfo"
)

type AuthScope string

func (s AuthScope) String() string {
	return string(s)
}

type Client struct {
	client *req.Client

	appID     string
	appSecret string
	token     string
	aesKey    string

	verifyKey   string
	verifyValue string
}

func New(options ...Options) *Client {
	we := &Client{
		client: DefaultClient,
	}

	for _, opt := range options {
		opt(we)
	}

	return we
}

func (we *Client) VerifySite(key string) (string, error) {
	if key == we.verifyKey {
		return we.verifyValue, nil
	}
	return "", errors.New("verify failed")
}

func (we *Client) Verify(signature, timestamp, nonce string) error {
	params := []string{signature, timestamp, nonce, we.token}
	sort.Strings(params)
	str := strings.Join(params, "")
	hash := sha1.Sum([]byte(str))
	hashStr := hex.EncodeToString(hash[:])

	if hashStr == signature {
		return errors.New("Signature verification failed")
	}

	return nil
}

func (we *Client) wrapParams(params map[string]string) map[string]string {
	if params == nil {
		params = make(map[string]string)
	}

	params["appid"] = we.appID
	params["secret"] = we.appSecret

	return params
}

// RefreshAccessToken
func (we *Client) RefreshAccessToken(refreshToken string) (*AccessTokenResponse, error) {
	params := we.wrapParams(map[string]string{
		"grant_type":    "refresh_token",
		"refresh_token": refreshToken,
	})

	var data AccessTokenResponse
	_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/oauth2/refresh_token")
	if err != nil {
		return nil, errors.Wrap(err, "call /sns/oauth2/refresh_token failed")
	}

	if data.ErrCode != 0 {
		return nil, data.Error()
	}

	return &data, nil
}

func (we *Client) GetAccessToken() (*AccessTokenResponse, error) {
	params := map[string]string{
		"grant_type": "client_credential",
	}

	var data ErrorResponse
	resp, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/cgi-bin/token")
	if err != nil {
		return nil, errors.Wrap(err, "call /cgi-bin/token failed")
	}

	if data.ErrCode != 0 {
		return nil, data.Error()
	}

	var token AccessTokenResponse
	if err := resp.Unmarshal(&token); err != nil {
		return nil, errors.Wrap(err, "parse response failed")
	}

	return &token, nil
}

// ScopeAuthorizeURL
func (we *Client) ScopeAuthorizeURL(opts ...ScopeAuthorizeURLOptions) (*url.URL, error) {
	params := url.Values{}
	params.Add("appid", we.appID)
	params.Add("response_type", "code")

	for _, opt := range opts {
		opt(params)
	}

	if params.Get("scope") == "" {
		params.Add("scope", ScopeBase)
	}

	u, err := url.Parse("https://open.weixin.qq.com/connect/oauth2/authorize")
	if err != nil {
		return nil, errors.Wrap(err, "parse url failed")
	}

	u.Fragment = "wechat_redirect"
	u.RawQuery = url.Values(params).Encode()

	return u, nil
}

var _ oauth.OAuthInfo = (*AuthorizeAccessToken)(nil)

type AuthorizeAccessToken struct {
	ErrorResponse
	AccessToken    string `json:"access_token,omitempty"`
	ExpiresIn      int64  `json:"expires_in,omitempty"`
	IsSnapshotuser int64  `json:"is_snapshotuser,omitempty"`
	Openid         string `json:"openid,omitempty"`
	RefreshToken   string `json:"refresh_token,omitempty"`
	Scope          string `json:"scope,omitempty"`
	Unionid        string `json:"unionid,omitempty"`
}

// GetAccessToken implements oauth.OAuthInfo.
func (a *AuthorizeAccessToken) GetAccessToken() string {
	return a.AccessToken
}

// GetExpiredAt implements oauth.OAuthInfo.
func (a *AuthorizeAccessToken) GetExpiredAt() time.Time {
	return time.Now().Add(time.Duration(a.ExpiresIn) * time.Second)
}

// GetOpenID implements oauth.OAuthInfo.
func (a *AuthorizeAccessToken) GetOpenID() string {
	return a.Openid
}

// GetRefreshToken implements oauth.OAuthInfo.
func (a *AuthorizeAccessToken) GetRefreshToken() string {
	return a.RefreshToken
}

// GetUnionID implements oauth.OAuthInfo.
func (a *AuthorizeAccessToken) GetUnionID() string {
	return a.Unionid
}

type StableAccessToken struct {
	AccessToken string `json:"access_token,omitempty"`
	ExpiresIn   int64  `json:"expires_in,omitempty"`
}

func (we *Client) GetStableAccessToken() (*StableAccessToken, error) {
	params := we.wrapParams(map[string]string{
		"grant_type": "client_credential",
	})

	var data StableAccessToken
	_, err := we.client.R().SetSuccessResult(&data).SetBodyJsonMarshal(params).Post("/cgi-bin/stable_token")
	if err != nil {
		return nil, errors.Wrap(err, "call /cgi-bin/stable_token failed")
	}

	return &data, nil
}

func (we *Client) AuthorizeCode2Token(code string) (*AuthorizeAccessToken, error) {
	params := we.wrapParams(map[string]string{
		"code":       code,
		"grant_type": "authorization_code",
	})

	var data AuthorizeAccessToken
	_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/oauth2/access_token")
	if err != nil {
		return nil, errors.Wrap(err, "call /sns/oauth2/access_token failed")
	}

	if err := data.Error(); err != nil {
		return nil, err
	}

	return &data, nil
}

func (we *Client) AuthorizeRefreshAccessToken(accessToken string) (*AuthorizeAccessToken, error) {
	params := we.wrapParams(map[string]string{
		"refresh_token": accessToken,
		"grant_type":    "refresh_token",
	})

	var data AuthorizeAccessToken
	_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/oauth2/refresh_token")
	if err != nil {
		return nil, errors.Wrap(err, "call /sns/oauth2/refresh_token failed")
	}

	if err := data.Error(); err != nil {
		return nil, err
	}

	return &data, nil
}

type AuthorizeUserInfo struct {
	ErrorResponse
	City       string   `json:"city,omitempty"`
	Country    string   `json:"country,omitempty"`
	Headimgurl string   `json:"headimgurl,omitempty"`
	Nickname   string   `json:"nickname,omitempty"`
	Openid     string   `json:"openid,omitempty"`
	Privilege  []string `json:"privilege,omitempty"`
	Province   string   `json:"province,omitempty"`
	Sex        int64    `json:"sex,omitempty"`
	Unionid    string   `json:"unionid,omitempty"`
}

func (we *Client) AuthorizeUserInfo(accessToken, openID string) (*AuthorizeUserInfo, error) {
	params := (map[string]string{
		"access_token": accessToken,
		"openid":       openID,
	})

	var data AuthorizeUserInfo
	_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/sns/userinfo")
	if err != nil {
		return nil, errors.Wrap(err, "call /sns/userinfo failed")
	}

	if err := data.Error(); err != nil {
		return nil, err
	}

	return &data, nil
}

// GetJSTicket
func (we *Client) GetJSTicket(token string) (string, error) {
	var data struct {
		Errcode   int    `json:"errcode"`
		Errmsg    string `json:"errmsg"`
		Ticket    string `json:"ticket"`
		ExpiresIn int    `json:"expires_in"`
	}

	params := map[string]string{
		"access_token": token,
		"type":         "jsapi",
	}
	_, err := we.client.R().SetSuccessResult(&data).SetQueryParams(params).Get("/cgi-bin/ticket/getticket")
	if err != nil {
		return "", errors.Wrap(err, "call /cgi-bin/ticket/getticket failed")
	}

	if data.Errcode != 0 {
		return "", errors.New("get wechat ticket failed: " + data.Errmsg)
	}

	return data.Ticket, nil
}

type JsSDK struct {
	Debug     bool   `json:"debug"`
	AppID     string `json:"appId"`
	Timestamp int64  `json:"timestamp"`
	NonceStr  string `json:"nonceStr"`
	Signature string `json:"signature"`
}

// GetJSTicket
func (we *Client) GetJsSDK(token, url string) (*JsSDK, error) {
	sdk := &JsSDK{
		Debug:     false,
		AppID:     we.appID,
		Timestamp: time.Now().Unix(),
		NonceStr:  randomString(16),
		Signature: "",
	}
	// get ticket
	ticket, err := we.GetJSTicket(token)
	if err != nil {
		return nil, errors.Wrap(err, "get wechat ticket failed")
	}

	input := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s&timestamp=%d&url=%s", ticket, sdk.NonceStr, sdk.Timestamp, url)
	sdk.Signature = hashSha1(input)

	return sdk, nil
}
