package v1

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

	"github.com/spf13/viper"

	"github.com/pingcap-inc/security/pkg/rstorage"

	modelv1 "github.com/pingcap-inc/security/model/v1"

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

const (
	LarkOpenAPIBase = "https://open.feishu.cn/open-apis"
	QfeiOpenAPiBase = "https://open.qfei.cn/open-apis"
	oldSafeOpera    = "old-safe-opera-"
	safeOpera       = "safe-opera-"
	dataSafeKey     = "data-safe-"
	qfeiKey         = "qfei-"

	oldSafeType  = 0
	safeType     = 1
	dataSafeType = 2
	qfeiType     = 3
)

type commonService struct {
}

type ComSrv interface {
	GetLarkToken(ctx context.Context, creType int) (string, error)
	GetDataByReqLarkAPI(ctx context.Context, m, url string, body io.Reader, params map[string]string, ttoken string) (json.RawMessage, error)
}

var _ ComSrv = (*commonService)(nil)

// 唯一的实例
var cmSrv *commonService

func newCommon() *commonService {
	if cmSrv == nil {
		cmSrv = &commonService{}
	}

	return cmSrv
}

func (cs *commonService) GetLarkToken(ctx context.Context, creType int) (string, error) {
	log.L(ctx).Info("GetLarkToken Called.")

	rds := &rstorage.RedisCluster{
		KeyPrefix: oldSafeOpera,
	}

	var tok, rdsKey string
	var err error

	switch creType {
	case dataSafeType:
		tok, err = rds.GetKey(dataSafeKey)
		rdsKey = dataSafeKey

	case safeType:
		tok, err = rds.GetKey(safeOpera)
		rdsKey = safeOpera
	case oldSafeType:
		tok, err = rds.GetKey(oldSafeOpera)
		rdsKey = oldSafeOpera
	case qfeiType:
		tok, err = rds.GetKey(qfeiKey)
		rdsKey = qfeiKey
	default:
		return "", errors.New("creType is invalid")
	}

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

	if tok == "" {
		var appIDInLark, appSecInLark string

		bUrl := LarkOpenAPIBase
		tatBody, _ := json.Marshal(map[string]string{
			"app_id":     appIDInLark,
			"app_secret": appSecInLark,
		})
		switch creType {
		case dataSafeType:
			appIDInLark = viper.GetString("larkdata.appId")
			appSecInLark = viper.GetString("larkdata.appSecret")
		case safeType:
			appIDInLark = viper.GetString("lark.appId")
			appSecInLark = viper.GetString("lark.appSecret")
		case oldSafeType:
			appIDInLark = viper.GetString("old-lark.appId")
			appSecInLark = viper.GetString("old-lark.appSecret")
		case qfeiType:
			appIDInLark = viper.GetString("qfei.appId")
			appSecInLark = viper.GetString("qfei.appSecret")
			bUrl = QfeiOpenAPiBase
			tatBody, _ = json.Marshal(map[string]string{
				"appId":     appIDInLark,
				"appSecret": appSecInLark,
			})
		default:
			return "", errors.New("creType is invalid")
		}

		if appIDInLark == "" || appSecInLark == "" {
			err := errors.New("lark-app-id or lark-app-secret is empty.")
			log.L(ctx).Error(err.Error())

			return "", err
		}

		tatReqBody := bytes.NewBuffer(tatBody)
		tatReq, _ := http.NewRequestWithContext(ctx, http.MethodPost, bUrl+"/auth/v3/tenant_access_token/internal", tatReqBody)
		tatReq.Header.Set("Content-Type", "application/json; charset=utf-8")
		tatResp, err := (&http.Client{}).Do(tatReq)
		if err != nil {
			err := errors.Wrap(err, "error when request lark tenant access token")
			log.L(ctx).Error(err.Error())

			return "", err
		}
		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.Wrap(err, "error when unmarshal lark tenant_access_token res")
		}
		tok = tatRes.TAT

		if err := rds.SetKey(rdsKey, tok, 30*time.Minute); err != nil {
			log.L(ctx).Errorf("Error when store multi-sa token into redis: %s", err.Error())
		}
	}

	return tok, nil
}

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

	tat := ttoken
	if tat == "" {
		rtat, err := cs.GetLarkToken(ctx, 1)
		if err != nil {
			log.L(ctx).Error(err.Error())

			return nil, errors.Wrap(err, "Error when GetLarkToken")
		}
		tat = rtat
	}

	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)
	req.Header.Set("Content-Type", "application/json")
	resp, err := (&http.Client{}).Do(req)
	if err != nil {
		log.L(ctx).Error(err.Error())
		err = fmt.Errorf("error when request lark api: %s %s, err: %w", m, url, err)

		return nil, err
	}
	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())
		err = fmt.Errorf("error when unmarshal lark api res: %s %s, err: %w", m, url, err)

		return nil, err
	}
	if res.Code != 0 {
		log.L(ctx).Errorf("Lark API error code: %d, msg: %s", res.Code, res.Msg)
		err = fmt.Errorf("lark Error Code: %d", res.Code)

		return nil, err
	}

	return res.Data, nil
}
