package receive

import (
	"context"
	"strings"
	"time"

	"github.com/tidbcloud/account/commons/eventbus"

	apikey2 "github.com/tidbcloud/account/internel/svc/apikey"
	dbuserSvc "github.com/tidbcloud/account/internel/svc/dbuser"
	"github.com/tidbcloud/account/internel/svc/user"

	"github.com/google/uuid"
	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/thirdparty/infra"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

func RegisterDataAppBindClusterEventHandler(dbUserSrv dbuserSvc.DBUserIface) error {
	eventHandlerFunc := func(e *eventbus.Event) error {
		return receiveDataAppBindClusterEvent(e, dbUserSrv)
	}

	err := eventbus.Get().RegisterEventHandleFunc(config.EventTypeDataAppBindCluster, config.Group, DataAppBindClusterEvent{}, eventHandlerFunc)
	if err != nil {
		tilog.Error(context.Background(), "RegisterDataAppBindClusterEventHandleFunc", zap.Error(err))
		return err
	}

	return nil
}

type DataAppBindClusterAction string

const (
	DataAppBindClusterActionAddApiKey  DataAppBindClusterAction = "add_api_key"
	DataAppBindClusterActionDelApiKey  DataAppBindClusterAction = "delete_api_key"
	DataAppBindClusterActionAddCluster DataAppBindClusterAction = "add_cluster"
	DataAppBindClusterActionDelCluster DataAppBindClusterAction = "delete_cluster"
)

var dataAppBindClusterAction2CommandActionMap = map[DataAppBindClusterAction]entity.CommandAction{
	DataAppBindClusterActionAddApiKey:  entity.CommandActionCreate,
	DataAppBindClusterActionDelApiKey:  entity.CommandActionDelete,
	DataAppBindClusterActionAddCluster: entity.CommandActionCreate,
	DataAppBindClusterActionDelCluster: entity.CommandActionDelete,
}

const (
	maxConsumeDelay = time.Second * 30
	emailToken      = "@"
)

type DataAppBindClusterEvent struct {
	EventType    DataAppBindClusterAction `json:"event_type"`
	ApiKeyID     uint64                   `json:"api_key_id"`
	DataAppID    uint64                   `json:"dataapp_id"`
	ClusterIDs   []uint64                 `json:"cluster_ids"`
	Operator     string                   `json:"operator"`
	Optime       int64                    `json:"optime"`
	ApiKeyIDList []uint64                 `json:"api_key_id_list"`
}

func receiveDataAppBindClusterEvent(event *eventbus.Event, dbUserSrv dbuserSvc.DBUserIface) error {
	ctx := context.Background()
	data := event.EventData.(*DataAppBindClusterEvent)
	traceID := uuid.New().String()
	tilog.Info(ctx, "receiveDataAppBindClusterEvent", zap.String("trace_id", traceID), zap.Any("eventData", data))

	// optime check
	createTime := time.UnixMilli(data.Optime)
	duration := time.Since(createTime)
	if duration > maxConsumeDelay {
		tilog.Warn(ctx, "receiveDataAppBindClusterEvent consume delay", zap.String("trace_id", traceID), zap.Any("duration", duration))
	}

	// check event_type
	commandAction, ok := dataAppBindClusterAction2CommandActionMap[data.EventType]
	if !ok {
		tilog.Info(ctx, "receiveDataAppBindClusterEvent, event_type invalid", zap.String("trace_id", traceID), zap.Any("eventData", data))
		return nil
	}

	// check clusters
	if len(data.ClusterIDs) == 0 {
		tilog.Warn(ctx, "receiveDataAppBindClusterEvent cluster_ids is empty", zap.String("trace_id", traceID))
		return nil
	}

	// check operator
	var operatorType entity.ClusterUserTargetType
	var operatorID uint64
	if strings.Contains(data.Operator, emailToken) {
		userSvc, _ := user.GetUserSvc()
		operateUser, err := userSvc.GetUserByEmail(ctx, data.Operator)
		if err != nil {
			tilog.Info(ctx, "receiveDataAppBindClusterEvent, GetUserByEmail err", zap.String("trace_id", traceID), zap.Any("err", err))
			return nil
		}
		operatorType = entity.ClusterUserTargetUser
		operatorID = operateUser.ID.Uint64()
	} else {
		keySvc, _ := apikey2.GetApiKeySvc()
		key, err := keySvc.GetApiKeyByAccessKey(ctx, data.Operator)
		if err != nil {
			tilog.Info(ctx, "receiveDataAppBindClusterEvent, GetApiKeyByAccessKey err", zap.String("trace_id", traceID), zap.Any("err", err))
			return nil
		}
		if key == nil {
			tilog.Info(ctx, "receiveDataAppBindClusterEvent, GetApiKeyByAccessKey return nil", zap.String("trace_id", traceID), zap.Any("operator", data.Operator))
			return nil
		}
		operatorType = entity.ClusterUserTargetApiKey
		operatorID = key.Id
	}

	if data.EventType == DataAppBindClusterActionAddApiKey || data.EventType == DataAppBindClusterActionDelApiKey {
		apiKeyIds := getApiKeyIdsFromEvent(data)
		if len(apiKeyIds) > 0 {
			for keyID := range apiKeyIds {
				for _, clusterID := range data.ClusterIDs {
					createDBUserClusterCommand(ctx, operatorID, operatorType, keyID, clusterID, commandAction, traceID, dbUserSrv, createTime)
				}
			}
		}
	} else if data.EventType == DataAppBindClusterActionAddCluster || data.EventType == DataAppBindClusterActionDelCluster {
		keys, err := getDataAppRelateApiKeys(ctx, data.DataAppID)
		if err != nil {
			tilog.Error(ctx, "receiveDataAppBindClusterEvent getDataAppRelateApiKeys err", zap.String("trace_id", traceID), zap.Any("err", err))
			return nil
		}
		for _, key := range keys {
			for _, clusterID := range data.ClusterIDs {
				createDBUserClusterCommand(ctx, operatorID, operatorType, key.Id, clusterID, commandAction, traceID, dbUserSrv, createTime)
			}
		}

	}

	return nil
}

func getApiKeyIdsFromEvent(event *DataAppBindClusterEvent) map[uint64]struct{} {
	var ids = map[uint64]struct{}{}
	if event.ApiKeyID != 0 {
		ids[event.ApiKeyID] = struct{}{}
	}
	if len(event.ApiKeyIDList) > 0 {
		for _, id := range event.ApiKeyIDList {
			ids[id] = struct{}{}
		}
	}
	return ids
}

func getDataAppRelateApiKeys(ctx context.Context, dataappID uint64) ([]*apikey2.ApiKey, error) {
	apiKeySvc, _ := apikey2.GetApiKeySvc()
	param := apikey2.ListKeysByResourceIDParam{
		ScopeType:  entity.RoleBindingScopeDataAPP.String(),
		ResourceId: dataappID,
	}
	retApiKeys, err := apiKeySvc.ListApiKeysByResourceID(ctx, &param)
	if err != nil {
		return nil, err
	}
	return retApiKeys.Items, nil
}

func createDBUserClusterCommand(ctx context.Context, operatorID uint64, operatorType entity.ClusterUserTargetType, apiKeyID, clusterID uint64, commandAction entity.CommandAction, traceID string, dbUserSrv dbuserSvc.DBUserIface, createTime time.Time) {
	// get api key
	svc, _ := apikey2.GetApiKeySvc()
	keyEntity, err := svc.GetApiKeyByID(ctx, apiKeyID)
	if err != nil {
		tilog.Error(ctx, "receiveDataAppBindClusterEvent, GetApiKeyByID err", zap.String("trace_id", traceID), zap.Any("key", apiKeyID), zap.Any("cluster_id", clusterID), zap.Any("err", err))
		return
	}
	if keyEntity == nil {
		tilog.Info(ctx, "receiveDataAppBindClusterEvent, api key not exist ,ignore", zap.String("trace_id", traceID), zap.Any("key", apiKeyID), zap.Any("cluster_id", clusterID))
		return
	}
	//  get cluster
	cluster, err := infra.GetHandler().GetClusterById(ctx, clusterID, true)
	if err != nil {
		tilog.Info(ctx, "receiveDataAppBindClusterEvent, GetClusterById err", zap.String("trace_id", traceID), zap.Any("key", apiKeyID), zap.Any("cluster_id", clusterID), zap.Any("err", err))
		return
	}
	if cluster == nil {
		tilog.Info(ctx, "receiveDataAppBindClusterEvent, cluster not exist, ignore", zap.String("trace_id", traceID), zap.Any("key", apiKeyID), zap.Any("cluster_id", clusterID))
		return
	}
	supportDBUser := infra.IsSupportDBUser(ctx, cluster)
	if !supportDBUser {
		tilog.Info(ctx, "receiveDataAppBindClusterEvent, cluster not support db user, ignore", zap.String("trace_id", traceID), zap.Any("key", apiKeyID), zap.Any("cluster_id", clusterID))
		return
	}

	// gen db user name
	flag := !cluster.IsDevTier
	dbUser, err := utils.GetDBUser(ctx, utils.GetDBUserReq{
		Email:           keyEntity.AccessKey,
		ClusterId:       &cluster.Id,
		IsDedicated:     &flag,
		ServerlessToken: cluster.ServerlessToken,
		TargetType:      entity.RoleBindingTargetAPIKey.String(),
	})
	if err != nil {
		tilog.Error(ctx, "receiveDataAppBindClusterEvent, GetDBUser err", zap.String("trace_id", traceID), zap.Any("key", apiKeyID), zap.Any("key", apiKeyID), zap.Any("cluster_id", clusterID), zap.Any("err", err))
		return
	}
	// save db
	err = dbUserSrv.CreateCommandDBUserClusterWithTime(ctx, cluster.Id, cluster.TenantId, operatorID, keyEntity.Id, *dbUser, entity.CommandStatusWaiting, commandAction, entity.ClusterUserTargetApiKey, createTime, entity.DbRoleAdmin, operatorType)
	if err != nil {
		tilog.Error(ctx, "receiveDataAppBindClusterEvent, CreateCommandDBUserCluster err", zap.String("trace_id", traceID), zap.Any("key", apiKeyID), zap.Any("cluster_id", clusterID), zap.Any("err", err))
		return
	}
	tilog.Info(ctx, "receiveDataAppBindClusterEvent, CreateCommandDBUserCluster complete", zap.String("trace_id", traceID), zap.Any("key", apiKeyID), zap.Any("cluster_id", clusterID), zap.Any("db_user", dbUser))
}
