package v1

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

	"github.com/spf13/viper"
	"google.golang.org/api/iamcredentials/v1"

	"github.com/pingcap-inc/pchub/internal/apiserver/store"
	"github.com/pingcap-inc/pchub/internal/pkg/code"
	"github.com/pingcap-inc/pchub/internal/pkg/rstore"
	modelv1 "github.com/pingcap-inc/pchub/model/v1"
	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
	"github.com/pingcap-inc/pchub/pkg/rstorage"
)

const (
	TenantTkKey      = "pchub-tenant-token"
	LarkOpenAPIBase  = "https://open.feishu.cn/open-apis"
	CloudProviderKey = "cloud-provider"
	rmSA             = "pch-resource-manager@gcp-digital-infra.iam.gserviceaccount.com"
	bqSA             = "billing-readonly@gcp-digital-bi.iam.gserviceaccount.com"
	multiSATokenKey  = "multi-sa-token"
)

type commonService struct {
	store store.Factory
}

type ComSrv interface {
	GetAndCachePchubLarkToken(ctx context.Context) (string, error)
	GetDataByReqLarkAPI(ctx context.Context, m, url string, body io.Reader, params map[string]string) (json.RawMessage, error)
	GetCloudMap(ctx context.Context) map[string]modelv1.CloudProvider
	GetAuthMap(ctx context.Context) map[string]int
	GetManagementMap(ctx context.Context) map[string]int
	GetRoleMap(ctx context.Context) map[string]uint64
	GetApprovalMap(ctx context.Context) (map[string]int, map[int]string, map[int]string, error)
	GetMultiSAToken(ctx context.Context) (string, error)
}

var _ ComSrv = (*commonService)(nil)

func newCommon(srv *service) *commonService {
	return &commonService{store: srv.store}
}

func (cs *commonService) GetAndCachePchubLarkToken(ctx context.Context) (string, error) {
	log.L(ctx).Info("GetAndCachePchubLarkToken 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("PCHub 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, _ := ioutil.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 *commonService) GetDataByReqLarkAPI(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.GetAndCachePchubLarkToken(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, _ := ioutil.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
}

func (cs *commonService) GetCloudMap(ctx context.Context) map[string]modelv1.CloudProvider {
	cloudProviderMap := make(map[string]modelv1.CloudProvider)
	cloudProviders, err := cs.store.CloudProvider().Gets(ctx)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return nil
	}
	for _, each := range cloudProviders {
		cloudProviderMap[each.Name] = *each
	}

	return cloudProviderMap
}

func (cs *commonService) GetAuthMap(ctx context.Context) map[string]int {
	authTypeMap := map[string]int{
		"unknown": 0,
		"sso":     1,
		"iam":     2,
	}

	return authTypeMap
}

func (cs *commonService) GetManagementMap(ctx context.Context) map[string]int {
	managementTypeMap := map[string]int{
		"unknown":      0,
		"self-managed": 1,
		"it-managed":   2,
	}

	return managementTypeMap
}

func (cs *commonService) GetRoleMap(ctx context.Context) map[string]uint64 {
	roleTypeMap := make(map[string]uint64)
	roles, err := cs.store.CloudMemberRole().Gets(ctx)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return nil
	}
	for _, each := range roles {
		roleTypeMap[each.Code] = each.ID
	}

	return roleTypeMap
}

func (cs *commonService) GetApprovalMap(ctx context.Context) (map[string]int, map[int]string, map[int]string, error) {
	typeMap := make(map[string]int)
	codeMap := make(map[int]string)
	nameMap := make(map[int]string)
	roles, err := cs.store.ApprovalType().Gets(ctx)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return nil, nil, nil, err
	}
	for _, each := range roles {
		typeMap[each.Code] = each.ID
		codeMap[each.ID] = each.Code
		nameMap[each.ID] = each.Name
	}

	return typeMap, codeMap, nameMap, nil
}

func (cs *commonService) GetMultiSAToken(ctx context.Context) (string, error) {
	rStore := &rstorage.RedisCluster{
		KeyPrefix: rstore.PCHubKeyPrefix,
	}

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

	token := val
	if token == "" {
		iamcreSrv, err := iamcredentials.NewService(ctx)
		if err != nil {
			log.Errorf("error when getGoogleTokenSource, err: %s", err.Error())

			return "", err
		}
		resp, err := iamcredentials.NewProjectsServiceAccountsService(iamcreSrv).GenerateAccessToken("projects/-/serviceAccounts/"+rmSA, &iamcredentials.GenerateAccessTokenRequest{
			Scope:     []string{"https://www.googleapis.com/auth/cloud-platform"},
			Lifetime:  "3600s",
			Delegates: []string{bqSA},
		}).Context(ctx).Do()
		if err != nil {
			log.Errorf("error when getGoogleTokenSource, err: %s", err.Error())

			return "", err
		}

		token = resp.AccessToken
		if err := rStore.SetKey(multiSATokenKey, token, time.Hour); err != nil {
			log.L(ctx).Errorf("Error when store multi-sa token into redis: %s", err.Error())
		}
	}

	return token, nil
}
