package v1

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

	"github.com/pingcap-inc/pingcat/internal/apiserver/store"
	"github.com/pingcap-inc/pingcat/internal/pkg/code"
	"github.com/pingcap-inc/pingcat/internal/pkg/rstore"
	modelv1 "github.com/pingcap-inc/pingcat/model/v1"
	"github.com/spf13/viper"

	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
	"github.com/pingcap-inc/pchub/pkg/rstorage"
)

type LarkAPIService struct {
	store store.Factory
}

type LarkAPISrv interface {
	GetAndCacheLarkAppToken(ctx context.Context) (string, error)
	ReqLarkAPI(ctx context.Context, m, url string, body io.Reader, params map[string]string) (json.RawMessage, error)
}

var _ LarkAPISrv = (*LarkAPIService)(nil)

func NewLarkAPI(store store.Factory) *LarkAPIService {
	return &LarkAPIService{store: store}
}

const (
	TenantTkKey     = "pingcat-tenant-token"
	LarkOpenAPIBase = "https://open.feishu.cn/open-apis"
)

func (cs *LarkAPIService) GetAndCacheLarkAppToken(ctx context.Context) (string, error) {
	log.L(ctx).Info("LarkChatSrv.GetAndCacheLarkAppToken Called.")

	appIDInLark := viper.GetString("lark.appId")
	appSecInLark := viper.GetString("lark.appSecret")

	if appIDInLark == "" || appSecInLark == "" {
		log.L(ctx).Error("There is no appId or appSecret for pchub-lark-app")

		return "", errors.WithCode(code.ErrNoLarkAppIdSecret, "")
	}

	rStore := &rstorage.RedisCluster{
		KeyPrefix: rstore.LarkKeyPrefix,
	}

	val, err := rStore.GetKey(TenantTkKey)
	if err != nil {
		if !errors.As(err, &rstorage.ErrKeyNotFound) {
			log.L(ctx).Error(errors.Wrap(err, "error when retrieve key in redis").Error())
		}
	}

	tToken := val
	if tToken == "" {
		log.L(ctx).Info("PingCat tenant access token not found in redis. Try to get from api")

		tatBody, _ := json.Marshal(map[string]string{ //nolint:errchkjson // must Marshal
			"app_id":     appIDInLark,
			"app_secret": appSecInLark,
		})
		tatReqBody := bytes.NewBuffer(tatBody)
		tatReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, LarkOpenAPIBase+"/auth/v3/tenant_access_token/internal", tatReqBody)
		tatResp, err := (&http.Client{}).Do(tatReq)
		if err != nil {
			log.L(ctx).Error(err.Error())

			return "", errors.WithCode(code.ErrHttpRequest, "error when request lark api for tenant_access_toekn, err: %s", err.Error())
		}
		defer func() {
			_ = tatResp.Body.Close()
		}()
		tatRespBody, _ := io.ReadAll(tatResp.Body)
		var tatRes modelv1.LarkTATokenRes
		if err := json.Unmarshal(tatRespBody, &tatRes); err != nil {
			log.L(ctx).Errorf("Error when unmarshal lark res: %s", err.Error())

			return "", errors.WithCode(code.ErrDecodingJSON, "error when unmarshal lark tenant_access_token res")
		}
		tToken = tatRes.TAT
		if err := rStore.SetKey(TenantTkKey, tToken, time.Duration(tatRes.Expire)*time.Second-time.Minute*10); err != nil {
			log.L(ctx).Errorf("Error when store token into redis: %s", err.Error())
		}
	} else {
		log.L(ctx).Info("PCHub tenant access token found in redis. Use it from redis.")
	}

	return tToken, nil
}

func (cs *LarkAPIService) ReqLarkAPI(ctx context.Context, m, url string, body io.Reader, params map[string]string) (json.RawMessage, error) {
	log.L(ctx).Info("GetDataByReqLarkAPI Called.")

	tat, err := cs.GetAndCacheLarkAppToken(ctx)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return nil, errors.Wrap(err, "Error when GetAndCachePchubLarkToken")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	req, _ := http.NewRequestWithContext(ctx, m, url, body)

	query := req.URL.Query()
	if params != nil || len(params) != 0 {
		for k, v := range params {
			query.Set(k, v)
		}
	}
	req.URL.RawQuery = query.Encode()
	req.Header.Set("Authorization", "Bearer "+tat)
	resp, err := (&http.Client{}).Do(req)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return nil, errors.WithCode(code.ErrHttpRequest, "error when request lark api: %s %s, err: %s", m, url, err.Error())
	}
	defer func() {
		_ = resp.Body.Close()
	}()

	b, _ := io.ReadAll(resp.Body)
	var res modelv1.LarkCommonRes
	if err := json.Unmarshal(b, &res); err != nil {
		log.L(ctx).Error(err.Error())

		return nil, errors.WithCode(code.ErrDecodingJSON, "error when unmarshal lark api res: %s %s, err: %s", m, url, err.Error())
	}
	if res.Code != 0 {
		log.L(ctx).Errorf("Lark API error code: %d, msg: %s", res.Code, res.Msg)

		return nil, errors.WithCode(code.ErrLarkRequestFail, "Lark Error Code: %d", res.Code)
	}

	return res.Data, nil
}
