package jsm

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"net/http"
	"strings"
	"sync"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/sqs"
	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

type JsmClient struct {
	conf   *config.JsmConfig
	header map[string]string
	sqs    *sqs.SQS
}

const FIELD_SERVERITY = "customfield_10090"
const FIELD_IMPACT = "customfield_10102"
const FIELD_ENV = "customfield_10050"
const FIELD_ORGID = "customfield_10110"
const KEY_REPORTER = "reporter"
const APPLICATION_HEADER = "application/json"

var jsmCli *JsmClient
var jsmOnce sync.Once

// NOTE(bing.wang) JSM Client is used to create ticket in jsm system.
// API Doc: https://q1dqx87s19.feishu.cn/docx/XgkXd5VJ4ouqkpx3YxAc5aMcntb

func initialJsmClient() {
	jsmConfig := config.Global().JsmConfig
	jsmCli = &JsmClient{conf: &jsmConfig, header: make(map[string]string)}
	if !jsmConfig.Enable {
		return
	}

	jsmCli.header["Content-Type"] = APPLICATION_HEADER
	jsmCli.header["Accept"] = APPLICATION_HEADER
	encoded := base64.StdEncoding.EncodeToString([]byte(jsmConfig.User + ":" + jsmConfig.ApiToken))
	jsmCli.header["Authorization"] = "Basic " + encoded
	region := jsmConfig.JobQueueRegion
	sess := session.Must(session.NewSessionWithOptions(session.Options{
		Config: aws.Config{
			Region: &region,
		},
		SharedConfigState: session.SharedConfigStateFromEnv,
	}))
	jsmCli.sqs = sqs.New(sess)
	tilog.Info(context.Background(), "init jsm SQS client obj success. ", zap.String("region", region), zap.Any("shared_config_state", session.SharedConfigStateFromEnv))
}

func GetJsmClientSingleton() *JsmClient {
	jsmOnce.Do(func() {
		initialJsmClient()
	})
	return jsmCli
}

func MockNewJsmClient(config *config.JsmConfig) *JsmClient {
	cli := JsmClient{conf: config, header: make(map[string]string)}
	cli.header["Content-Type"] = "application/json"
	cli.header["Accept"] = "application/json"
	encoded := base64.StdEncoding.EncodeToString([]byte(config.User + ":" + config.ApiToken))
	cli.header["Authorization"] = "Basic " + encoded
	return &cli
}

func (c *JsmClient) ProcessSSOUserRegistration(ctx context.Context, user *entity.User, orgID uint64) {
	if c.IsEnable() {
		_, createJsmAccountErr := c.CreateJsmAccount(ctx, user.Email)
		notifyCreateUserErr := c.NotifyCreateUserMessage(ctx, user)
		notifyInviteUserToOrgErr := c.NotifyInviteUserToOrgMessage(ctx, orgID, user.ID.Uint64(), entity.RoleOrgMember)
		if createJsmAccountErr != nil || notifyCreateUserErr != nil || notifyInviteUserToOrgErr != nil {
			tilog.Warn(ctx, "Sync Jsm failed in new user registration", zap.Uint64("user", user.ID.Uint64()), zap.Uint64("org", orgID))
		}
	}
}

func (c *JsmClient) IsEnable() bool {
	return c.conf.Enable
}

func (c *JsmClient) expAPIHeader() *map[string]string {
	header := make(map[string]string)
	for k, v := range c.header {
		header[k] = v
	}
	header["X-ExperimentalApi"] = "opt-in"
	return &header
}
func (c *JsmClient) GetAssertTypeEnv(ctx context.Context, label string) (string, bool, error) {
	return label, true, nil
}
func (c *JsmClient) GetAssertTypeSeverity(ctx context.Context, serverityLabel string) (string, bool, error) {
	resp, err := c.GetAssertField(ctx, FIELD_SERVERITY, make(map[string]string))
	if err != nil {
		tilog.Error(ctx, "GetAssertField failed. ", zap.Any("error", err))
		return "", false, err
	}
	for _, value := range resp.Results {
		if value.Label == serverityLabel {
			return value.ID, true, nil
		}
	}
	tilog.Error(ctx, "label can not be matched in jsm response", zap.String("label", serverityLabel), zap.Any("jsmResp", resp))
	return "", false, nil
}

func (c *JsmClient) GetAssertTypeImpact(ctx context.Context, serverityID string) (string, bool, error) {
	fieldValueMap := make(map[string]string)
	fieldValueMap[FIELD_SERVERITY] = serverityID

	resp, err := c.GetAssertField(ctx, FIELD_IMPACT, fieldValueMap)
	if err != nil {
		tilog.Error(ctx, "GetAssertField failed. ", zap.Any("error", err))
		return "", false, err
	}
	// XXX(bing.wang) return the first selection directly.
	for _, value := range resp.Results {
		return value.ID, true, nil
	}
	tilog.Error(ctx, "no value found in jsm response", zap.String("serverityID", serverityID), zap.Any("jsmResp", resp))
	return "", false, nil
}

func (c *JsmClient) createPingcapAccount(ctx context.Context, email string) (string, error) {
	url := c.conf.Domain + "/rest/api/3/user"
	request := createPingcapAccountRequest{
		DisplayName: email, EmailAddress: email, Name: email, Products: []string{},
	}
	ret, httpCode, err := utils.PostWithHeader(url, request, &c.header)
	if err != nil {
		tilog.Error(ctx, "createPingcapAccount  failed,", zap.String("err:", err.Error()))
		return "", err
	}
	var response createAccountResponse
	err = json.Unmarshal([]byte(ret), &response)
	if err != nil {
		tilog.Warn(ctx, "parse response struct failed.", zap.String("err:", err.Error()))
		return "", err
	}
	tilog.Info(ctx, "createPingcapAccount result: ", zap.String("url: ", url), zap.Any("params: ", request), zap.Int("code: ", httpCode), zap.String("response: ", ret))
	return response.AccountID, nil
}

func (c *JsmClient) createNormalAccount(ctx context.Context, email string) (string, error) {
	url := c.conf.Domain + "/rest/servicedeskapi/customer"
	request := createNormalAccountRequest{
		DisplayName: email, Email: email,
	}
	ret, httpCode, err := utils.PostWithHeader(url, request, &c.header)
	if err != nil {
		tilog.Error(ctx, "createPingcapAccount  failed,", zap.String("err:", err.Error()))
		return "", err
	}
	tilog.Info(ctx, "createPingcapAccount result: ", zap.String("url: ", url), zap.Any("params: ", request), zap.Int("code: ", httpCode), zap.String("response: ", ret))
	var response createAccountResponse
	err = json.Unmarshal([]byte(ret), &response)
	if err != nil {
		tilog.Warn(ctx, "parse response struct failed.", zap.String("err:", err.Error()))
		return "", err
	}
	return response.AccountID, nil
}
func (c *JsmClient) addAccountToJsmGroup(ctx context.Context, accountId string) (string, error) {
	url := c.conf.Domain + "/rest/api/2/group/user?groupname=" + c.conf.CustomerGroup
	request := addAccountToJsmGroupRequest{AccountID: accountId}
	ret, httpCode, err := utils.PostWithHeader(url, request, &c.header)
	if err != nil {
		tilog.Error(ctx, "addAccountToJsmGroup  failed,", zap.String("err:", err.Error()))
		return "", err
	}
	tilog.Info(ctx, "addAccountToJsmGroup result: ", zap.String("url: ", url), zap.Any("params: ", request), zap.Int("code: ", httpCode), zap.String("response: ", ret))
	if httpCode != 201 {
		return "", errs.NewBadRequest("addAccountToJsmGroup Failed.", nil)
	}
	return accountId, nil
}
func (c *JsmClient) CreateJsmAccount(ctx context.Context, email string) (string, error) {
	accountId, isExist, err := c.GetAccountID(ctx, email)
	if err != nil {
		return "", err
	}
	if isExist {
		return accountId, nil
	}
	if strings.HasSuffix(email, "@pingcap.com") {
		accountId, err := c.createPingcapAccount(ctx, email)
		if err != nil {
			return "", err
		}
		_, err = c.addAccountToJsmGroup(ctx, accountId)
		if err != nil && errors.Is(err, errs.ErrBadRequest) {
			return accountId, nil
		}
		return accountId, err

	}
	return c.createNormalAccount(ctx, email)
}

func (c *JsmClient) GetRequestTypeFields(ctx context.Context) (*RequestTypeResponse, error) {
	url := c.conf.Domain + "/rest/servicedeskapi/servicedesk/" + c.conf.ServiceDeskId + "/requesttype/" + c.conf.RequestTypeId + "/field"
	ret, httpCode, err := utils.GetWithHeader(url, nil, &c.header)
	if err != nil {
		tilog.Error(ctx, "GetRequestTypeFields  failed,", zap.String("err:", err.Error()))
		return nil, err
	}
	tilog.Info(ctx, "GetRequestTypeFields result: ", zap.String("url: ", url), zap.Int("code: ", httpCode), zap.String("response: ", ret))

	var response RequestTypeResponse
	err = json.Unmarshal([]byte(ret), &response)
	if err != nil {
		tilog.Error(ctx, "parse response struct failed.", zap.String("err:", err.Error()))
		return nil, err
	}
	return &response, nil
}

func (c *JsmClient) GetAssertField(ctx context.Context, fieldId string, fieldValueMap map[string]string) (*GetAssertFieldResponse, error) {
	url := c.conf.Domain + "/rest/servicedesk/cmdb/1/customer/portal/" + c.conf.ServiceDeskId + "/request/" + c.conf.RequestTypeId + "/field/" + fieldId + "/autocomplete"
	request := GetAssertFieldRequest{FieldValueMap: fieldValueMap}
	ret, httpCode, err := utils.PostWithHeader(url, request, &c.header)
	if err != nil {
		tilog.Info(ctx, "GetAssertField failed,", zap.String("err:", err.Error()))
		return nil, err
	}
	tilog.Info(ctx, "GetAssertField result: ", zap.String("url: ", url), zap.Any("params: ", request), zap.Int("code: ", httpCode), zap.String("response: ", ret))
	var response GetAssertFieldResponse
	err = json.Unmarshal([]byte(ret), &response)
	if err != nil {
		tilog.Error(ctx, "parse response struct failed.", zap.String("err:", err.Error()))
		return nil, err
	}
	tilog.Info(ctx, "response is", zap.Any("resp", response))
	return &response, nil
}

func (c *JsmClient) parseValueFromRequestTypeFields(ctx context.Context, fields []Field, fieldId string, valueLabel string) (string, bool) {
	for _, field := range fields {
		if field.FieldID != fieldId {
			continue
		}
		for _, validValue := range field.ValidValues {
			if validValue.Label == valueLabel {
				return validValue.Value, true
			}
		}
	}
	return "", false
}

func (c *JsmClient) getTiDBVersionValue(clusterType string) string {
	if clusterType == "Dedicated" {
		return "TiDB-Cloud-Dedicated"
	} else if clusterType == "Serverless" {
		return "TiDB-Cloud-Serverless"
	}
	return "TiDB-Cloud-NoVersion"
}

func (c *JsmClient) convertInputIDs(ctx context.Context, param *CreateJsmParam) (*CreateJsmParamConvertIDs, error) {
	convertIds := CreateJsmParamConvertIDs{}
	respObj, err := c.GetRequestTypeFields(ctx)
	if err != nil {
		tilog.Error(ctx, "GetRequestTypeFields failed.", zap.Any("err", err))
		return nil, err
	}
	tilog.Info(ctx, "respObj is ", zap.Any("resp", respObj))
	isFound := false
	convertIds.ComponentID, isFound = c.parseValueFromRequestTypeFields(ctx, respObj.RequestTypeFields, "components", "TiDB-Cloud")
	if !isFound {
		return nil, errs.NewBadRequest("no component id found in JSM response", nil)
	}
	convertIds.VersionID, isFound = c.parseValueFromRequestTypeFields(ctx, respObj.RequestTypeFields, "versions", c.getTiDBVersionValue(param.ClusterType))
	if !isFound {
		return nil, errs.NewBadRequest("no version id found in JSM response", nil)
	}

	convertIds.EnvironmentID, isFound, err = c.GetAssertTypeEnv(ctx, "production")
	if err != nil {
		tilog.Error(ctx, "GetAssertTypeEnv failed.", zap.Any("err", err))
		return nil, err
	}
	if !isFound {
		return nil, errs.NewBadRequest("no EnvironmentID found in JSM response", nil)
	}

	convertIds.SeverityID, isFound, err = c.GetAssertTypeSeverity(ctx, param.Severity)
	if err != nil {
		tilog.Error(ctx, "GetAssertTypeSeverity failed.", zap.Any("err", err))
		return nil, err
	}
	if !isFound {
		return nil, errs.NewBadRequest("no serverityID found in JSM response", nil)
	}

	convertIds.ImpactID, isFound, err = c.GetAssertTypeImpact(ctx, convertIds.SeverityID)
	if err != nil {
		tilog.Error(ctx, "GetAssertTypeSeverity failed.", zap.Any("err", err))
		return nil, err
	}
	if !isFound {
		return nil, errs.NewBadRequest("no serverityID found in JSM response", nil)
	}

	return &convertIds, nil
}

func (c *JsmClient) AddAttachement(ctx context.Context, issueId string, attachmentIds []string) error {
	url := c.conf.Domain + "/rest/servicedeskapi/request/" + issueId + "/attachment"
	request := AddAttachmentRequest{Public: true, TemporaryAttachmentIds: attachmentIds}
	// request := AddAttachmentRequest{Public: true, AdditionalComment: AdditionalComment{Body: "auto upload"}, TemporaryAttachmentIds: attachmentIds}
	ret, httpCode, err := utils.PostWithHeader(url, request, &c.header)
	tilog.Info(ctx, "AddAttachement result: ", zap.String("url: ", url), zap.Any("params: ", request), zap.Int("code: ", httpCode), zap.String("response: ", ret))
	if err != nil {
		tilog.Error(ctx, "AddAttachement failed,", zap.String("err:", err.Error()))
		return err
	}
	if httpCode < http.StatusOK || httpCode > http.StatusMultipleChoices {
		tilog.Error(ctx, "AddAttachement failed,", zap.Int("httpCode:", httpCode))
		return errs.NewBadRequest("add attachment to request failed.", nil)
	}
	return nil
}

func (c *JsmClient) GetCustomFieldId(ctx context.Context, key string) (string, bool, error) {
	respObj, err := c.GetRequestTypeFields(ctx)
	if err != nil {
		tilog.Error(ctx, "GetRequestTypeFields failed.", zap.Any("err", err))
		return "", false, err
	}
	tilog.Info(ctx, "respObj is ", zap.Any("resp", respObj))

	for _, field := range respObj.RequestTypeFields {
		if field.Name == key {
			return field.FieldID, true, nil
		}
	}
	return "", false, nil
}
func (c *JsmClient) GetClusterIDs(ctx context.Context, jsmAccountId, jsmFieldId, jsmOrgId string) (string, map[string]string, error) {
	var res map[string]string
	fieldValueMap := make(map[string]string)
	fieldValueMap[jsmFieldId] = jsmOrgId
	fieldValueMap[KEY_REPORTER] = jsmAccountId

	fieldId, isFound, err := c.GetCustomFieldId(ctx, "TiDB Cloud Cluster")
	if err != nil {
		tilog.Error(ctx, "GetCustomFieldId failed. ", zap.Any("error", err))
		return "", res, err
	}
	if !isFound {
		tilog.Error(ctx, "GetCustomFieldId failed. not found ")
		return "", res, errs.NewBadRequest("no orgid field_id found.", nil)
	}

	resp, err := c.GetAssertField(ctx, fieldId, fieldValueMap)
	if err != nil {
		tilog.Error(ctx, "GetAssertField failed. ", zap.Any("error", err))
		return "", res, err
	}
	res = make(map[string]string)
	for _, value := range resp.Results {
		res[value.Label] = value.ID
	}
	return fieldId, res, nil
}
func (c *JsmClient) GetOrgIDs(ctx context.Context, accountId string) (string, map[string]string, error) {
	var res map[string]string

	fieldValueMap := make(map[string]string)
	fieldValueMap[KEY_REPORTER] = accountId

	filedId, isFound, err := c.GetCustomFieldId(ctx, "TiDB Cloud Org")
	// filedId, isFound, err := c.GetCustomFieldId(ctx, "TiDB Cloud Orgnization")
	if err != nil {
		tilog.Error(ctx, "GetCustomFieldId failed. ", zap.Any("error", err))
		return "", res, err
	}
	if !isFound {
		tilog.Error(ctx, "GetCustomFieldId failed. not found ")
		return "", res, errs.NewBadRequest("no orgid field_id found.", nil)
	}

	resp, err := c.GetAssertField(ctx, filedId, fieldValueMap)
	if err != nil {
		tilog.Error(ctx, "GetAssertField failed. ", zap.Any("error", err))
		return "", res, err
	}
	res = make(map[string]string)
	for _, value := range resp.Results {
		res[value.Label] = value.ID
	}
	return filedId, res, nil
}
func (c *JsmClient) GetAccountID(ctx context.Context, userEmail string) (string, bool, error) {
	url := c.conf.Domain + "/rest/servicedeskapi/servicedesk/" + c.conf.ServiceDeskId + "/customer"
	params := make(map[string]string)
	params["query"] = userEmail
	ret, httpCode, err := utils.GetWithHeader(url, &params, c.expAPIHeader())
	tilog.Info(ctx, "GetAccountID result", zap.Any("url", url), zap.Any("params", params), zap.Any("httpCode", httpCode), zap.Any("ret", ret), zap.Any("err", err))
	if err != nil {
		tilog.Error(ctx, "GetAccountID  failed,", zap.String("err:", err.Error()))
		return "", false, err
	}
	var response GetAccountIDResponse
	err = json.Unmarshal([]byte(ret), &response)
	if err != nil {
		tilog.Error(ctx, "parse response struct failed.", zap.String("err:", err.Error()))
		return "", false, err
	}
	if len(response.Values) < 1 {
		tilog.Info(ctx, "No account found in jsm", zap.String("resp: ", ret))
		return "", false, nil
	}
	//tilog.Info(ctx, "GetAccountID result: ", zap.String("url: ", url), zap.Any("params: ", params), zap.Int("code: ", httpCode), zap.String("response: ", ret))

	return response.Values[0].AccountId, true, nil
}
