package feishuapi

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	gocache "github.com/patrickmn/go-cache"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	httpwrapper "github.com/pingcap-inc/clinic/tools/cleanup_user/http_wrapper"
)

const (
	openAPIEndpoint      = "https://open.feishu.cn"
	tenantAccessTokenKey = "tenant_access_token"
)

const (
	fetchTenantAccessTokenPath = "/open-apis/auth/v3/tenant_access_token/internal"
	batchGetUserIDPath         = "/open-apis/contact/v3/users/batch_get_id"
	getUserInfoPath            = "/open-apis/contact/v3/users/%s"
)

type Client struct {
	appID     string
	appSecret string

	tenantAccessTokenCache *gocache.Cache
	httpCli                *httpwrapper.Client
}

func New(appID, appSecret string) (*Client, error) {
	httpCli, err := httpwrapper.New()
	if err != nil {
		return nil, err
	}

	cli := &Client{
		appID:                  appID,
		appSecret:              appSecret,
		tenantAccessTokenCache: gocache.New(3600*time.Second, 3601*time.Second),
		httpCli:                httpCli,
	}

	return cli, nil
}

func (cli *Client) fetchTenantAccessToken(ctx context.Context) (string, error) {
	param := FetchTenantAccessTokenParam{
		AppID:     cli.appID,
		AppSecret: cli.appSecret,
	}
	ret := new(FetchTenantAccessTokenData)
	u := fmt.Sprintf("%s%s", openAPIEndpoint, fetchTenantAccessTokenPath)
	if err := cli.httpCli.CallWithJSON(ctx, ret, http.MethodPost, u, param); err != nil {
		return "", err
	}
	if err := ret.BizError(); err != nil {
		return "", err
	}
	return ret.TenantAccessToken, nil
}

func (cli *Client) getTenantAccessToken(ctx context.Context) (string, error) {
	if token, err := cli.getTenantAccessTokenFromCache(ctx); err == nil && len(token) > 0 {
		return token, nil
	}
	// fetch new access token from auth0 service.
	token, err := cli.fetchTenantAccessToken(ctx)
	if err != nil {
		return "", err
	}
	// do not abort the process on error
	if err := cli.setTenantAccessTokenToCache(ctx, token); err != nil {
		log.Warn("set auth0 access token failed", zap.Error(err))
	}
	return token, nil
}

func (cli *Client) refreshAccessToken(ctx context.Context) (string, error) {
	token, err := cli.getTenantAccessToken(ctx)
	if err != nil {
		return "", err
	}
	// do not abort the process on error
	if err := cli.setTenantAccessTokenToCache(ctx, token); err != nil {
		log.Warn("set auth0 access token failed", zap.Error(err))
	}
	return token, nil
}

func (cli *Client) getTenantAccessTokenFromCache(_ context.Context) (string, error) {
	item, found := cli.tenantAccessTokenCache.Get(tenantAccessTokenKey)
	if !found || item == nil {
		return "", nil
	}
	token, ok := item.(string)
	if !ok {
		return "", nil
	}
	return token, nil
}

func (cli *Client) setTenantAccessTokenToCache(_ context.Context, accessToken string) error {
	cli.tenantAccessTokenCache.Set(tenantAccessTokenKey, accessToken, 3600*time.Second)
	return nil
}

func (cli *Client) GetUserID(ctx context.Context, email string) (string, bool, error) {
	token, err := cli.getTenantAccessToken(ctx)
	if err != nil {
		return "", false, err
	}
	hd := make(map[string]string)
	hd["Authorization"] = "Bearer " + token
	param := BatchGetUserIDParam{Emails: []string{email}}

	u := fmt.Sprintf("%s%s", openAPIEndpoint, batchGetUserIDPath)
	ret := new(BatchGetUserIDResult)
	if err := cli.httpCli.CallWithJSONHeader(ctx, ret, http.MethodPost, u, param, hd); err != nil {
		return "", false, err
	}
	if err := ret.BizError(); err != nil {
		return "", false, err
	}
	if len(ret.Data.UserList) == 0 {
		return "", false, nil
	}
	if len(ret.Data.UserList[0].UserID) == 0 {
		return "", false, nil
	}
	return ret.Data.UserList[0].UserID, true, nil
}

func (cli *Client) GetUserInfo(ctx context.Context, openID string) (*GetUserInfoData, error) {
	token, err := cli.getTenantAccessToken(ctx)
	if err != nil {
		return nil, err
	}
	hd := make(map[string]string)
	hd["Authorization"] = "Bearer " + token

	u := fmt.Sprintf("%s%s", openAPIEndpoint, fmt.Sprintf(getUserInfoPath, openID))
	ret := new(GetUserInfoResult)

	resp, err := cli.httpCli.DoRequestWithFormHeader(ctx, http.MethodGet, u, nil, hd)
	if err != nil {
		return nil, err
	}
	defer func() {
		_, _ = io.Copy(io.Discard, resp.Body)
		_ = resp.Body.Close() // must close http body
	}()
	// status code is not ok.
	if resp.StatusCode >= 300 {
		base := new(BaseResult)
		_ = json.NewDecoder(resp.Body).Decode(base)
		return nil, fmt.Errorf("got abnormal status %d, ret %v", resp.StatusCode, base)
	}
	// check biz code error
	if err := json.NewDecoder(resp.Body).Decode(ret); err != nil {
		return nil, err
	}

	if err := ret.BizError(); err != nil {
		return nil, err
	}
	return ret.Data, nil
}
