package handler

import (
	"context"
	"crypto/md5"
	"encoding/json"
	"net/url"
	"sort"
	"strings"

	"encoding/base64"
	"encoding/hex"

	ali_mns "github.com/aliyun/aliyun-mns-go-sdk"
	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/thirdparty/billing2"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	CREATE_INSTANCE_ACTION  = "createInstance"
	RELEASE_INSTANCE_ACTION = "releaseInstance"
	VERIFY_ACTION           = "verify"
)

type aliyunAccountProviderImpl struct{}

type MNSMessageBody struct {
	AliUid      string `json:"aliUid"`
	Action      string `json:"action"`
	BizId       string `json:"bizId"`
	InTrial     bool   `json:"inTrial"`
	ProductCode string `json:"productCode"`
}

func (*aliyunAccountProviderImpl) ResolveToken(token *string) (*string, error) {
	return token, nil
}

func (*aliyunAccountProviderImpl) ApproveCode(code *string) error {
	_, err := billing2.GetHandler().GetActiveSubscriptionRecordByAliUid(context.Background(), *code)
	if err != nil {
		return errs.NewInternalServerError("Failed to approve your alibaba cloud marketplace accountID", err)
	}
	return nil
}

func (*aliyunAccountProviderImpl) GetTokenFromRedirect(c *gin.Context) (*string, error) {
	params := c.Request.URL.Query()
	if params.Get("action") != VERIFY_ACTION {
		return nil, errs.NewBadRequest("is not verify action", nil)
	}
	token := params.Get("token")
	if token == "" {
		return nil, errs.NewBadRequest("token parameter is missing", nil)
	}
	instanceId := params.Get("instanceId")
	tilog.Info(context.Background(), "aliyun GetTokenFromRedirect", zap.String("instanceId", instanceId))
	record, err := billing2.GetHandler().GetAliyunSubscriptionRecordByBizId(context.Background(), instanceId)
	if err != nil {
		tilog.Error(context.Background(), "aliyun GetAliyunSubscriptionRecordByBizId failed", zap.String("instanceId", instanceId), zap.Error(err))
		return nil, err
	}
	productCode := params.Get("productCode")
	if productCode == "" {
		productCode = record.ProductCode
	}
	params.Del("token")
	if !validateToken(params, token, productCode) {
		return nil, errs.NewBadRequest("token verification failed", nil)
	}
	return &record.AliUid, nil
}

func (*aliyunAccountProviderImpl) GetFreeTrialFromRedirect(c *gin.Context) (bool, error) {
	// default is false
	return false, nil
}

func (*aliyunAccountProviderImpl) ProcessTenantSubscribeChange(ctx context.Context, processor ProcessTenantSubscribeChangeFunc) error {
	client := ali_mns.NewAliMNSClientWithConfig(ali_mns.AliMNSClientConfig{
		EndPoint:        config.Global().MarketPlaceConfig.AliMNSEndpoint,
		AccessKeyId:     config.Global().MarketPlaceConfig.AliAccessKey,
		AccessKeySecret: config.Global().MarketPlaceConfig.AliSecretKey,
	})
	queue := ali_mns.NewMNSQueue(config.Global().MarketPlaceConfig.AliMNSQueueName, client)
	respChan := make(chan ali_mns.BatchMessageReceiveResponse)
	errChan := make(chan error)
	// query the message every 30s by global worker
	go queue.BatchReceiveMessage(respChan, errChan, 16, 30)
	// reveive and handle the message
	go func() {
		select {
		case resp := <-respChan:
			tilog.Info(ctx, "Receive batch mns messages", zap.Any("num", len(resp.Messages)))
			for _, msg := range resp.Messages {
				tilog.Info(ctx, "Receive mns message", zap.Any("msg", msg))
				processed, err := processOneAliyunSubscribeChange(ctx, msg, processor)
				if err != nil {
					tilog.Error(ctx, "Error processing mns message", zap.Error(err))
					continue
				}
				if !processed {
					tilog.Info(ctx, "MNS message processing ignored.", zap.Any("msg", msg))
					continue
				}
				// delete the message
				err = queue.DeleteMessage(msg.ReceiptHandle)
				if err != nil {
					tilog.Error(context.Background(), "Error deleting mns message", zap.Error(err))
				}
			}
		case err := <-errChan:
			if !ali_mns.ERR_MNS_MESSAGE_NOT_EXIST.IsEqual(err) {
				tilog.Error(ctx, "Error batch receive mns message", zap.Error(err))
			}
			tilog.Info(ctx, "No MNS messages received", zap.Error(err))
		}
	}()
	return nil
}

func processOneAliyunSubscribeChange(ctx context.Context, resp ali_mns.MessageReceiveResponse, processor ProcessTenantSubscribeChangeFunc) (bool, error) {
	change := SubscribeChange{}
	// parse the msg body
	jsonData, err := base64.StdEncoding.DecodeString(resp.MessageBody)
	if err != nil {
		return false, err
	}
	var msgBody MNSMessageBody
	err = json.Unmarshal(jsonData, &msgBody)
	if err != nil {
		return false, err
	}
	tilog.Info(ctx, "mns message parse success", zap.Any("msgBody", msgBody))
	change.AccountProvider = AliCloudAccountProvider
	switch msgBody.Action {
	case CREATE_INSTANCE_ACTION:
		subscriptionRecord, err := billing2.GetHandler().CreateAliyunSubscriptionRecord(ctx, msgBody.AliUid, msgBody.BizId, msgBody.InTrial, msgBody.ProductCode)
		if err != nil {
			return false, err
		}
		change.SubscribeStatus = SubscribeStatusSubscribed
		change.AccountCode = subscriptionRecord.AliUid
	case RELEASE_INSTANCE_ACTION:
		subscriptionRecord, err := billing2.GetHandler().InactiveAliyunSubscriptionRecord(ctx, msgBody.BizId)
		if err != nil {
			return false, err
		}
		change.SubscribeStatus = SubscribeStatusUnsubscribed
		change.AccountCode = subscriptionRecord.AliUid
	default:
		return true, nil
	}
	return processor(ctx, change)
}

// Validate the token
func validateToken(params url.Values, token string, productCode string) bool {
	ctx := context.Background()
	paramStr := buildParamString(params)
	isvKey := config.Global().MarketPlaceConfig.AliIsvKey
	isvKeySource := "default"
	if keyStr := config.Global().MarketPlaceConfig.AliIsvKeyStr; keyStr != "" {
		var m map[string]string
		if err := json.Unmarshal([]byte(keyStr), &m); err == nil {
			if key, ok := m[productCode]; ok {
				isvKey = key
				isvKeySource = "aliIsvKeyStr[" + productCode + "]"
			} else {
				tilog.Info(ctx, "aliyun validateToken: productCode not found in aliIsvKeyStr, using default isvKey", zap.String("productCode", productCode))
			}
		} else {
			tilog.Warn(ctx, "aliyun validateToken: failed to parse aliIsvKeyStr", zap.Error(err))
		}
	}
	tilog.Info(ctx, "aliyun validateToken", zap.String("productCode", productCode), zap.String("isvKeySource", isvKeySource), zap.String("paramStr", paramStr))
	finalStr := paramStr + "&key=" + isvKey
	md5Hash := md5.Sum([]byte(finalStr))
	md5Str := hex.EncodeToString(md5Hash[:])
	result := md5Str == token
	if !result {
		tilog.Error(ctx, "aliyun validateToken failed", zap.String("productCode", productCode), zap.String("isvKeySource", isvKeySource))
	}
	return result
}

// Arrange parameters in alphabetical order and concatenate into a string
func buildParamString(params url.Values) string {
	// Get all parameter names
	keys := make([]string, 0, len(params))
	for key := range params {
		keys = append(keys, key)
	}

	// Sort the keys alphabetically
	sort.Strings(keys)

	// Concatenate the parameters into a string
	var paramStr strings.Builder
	for _, key := range keys {
		values := params[key]
		for _, value := range values {
			paramStr.WriteString(key)
			paramStr.WriteString("=")
			paramStr.WriteString(value)
			paramStr.WriteString("&")
		}
	}
	// Remove the last "&"
	result := paramStr.String()
	if len(result) > 0 {
		result = result[:len(result)-1]
	}
	return result
}
