package metrics

import (
	"context"
	"errors"
	"math/rand"
	"strings"

	tokensvc "github.com/tidbcloud/account/internel/svc/token"

	"github.com/tidbcloud/account/internel/svc/monitor"

	authcenter2 "github.com/tidbcloud/account/internel/svc/authcenter"
	"github.com/tidbcloud/account/internel/svc/dbuser"
	historysvc "github.com/tidbcloud/account/internel/svc/history"
	rbacsvc "github.com/tidbcloud/account/internel/svc/rbac"
	user2 "github.com/tidbcloud/account/internel/svc/user"

	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/commons/metrics"
	"github.com/tidbcloud/account/commons/singleton"
	"github.com/tidbcloud/account/internel/dao/entity"

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

	"github.com/pingcap/errcode"

	"strconv"
	"time"

	"github.com/tidbcloud/account/idl/pbgen/proto/acl"
	"github.com/tidbcloud/account/idl/pbgen/proto/central"
	"github.com/tidbcloud/tiinf/monitor/tilog"

	"go.uber.org/zap"
)

const (
	SUCCESS = "SUCCESS"
	LOGIN   = "LOGIN"
)

/*
all error in Front-end

	export enum ErrorType {
		Server = 'server_error',
		Denied = 'access_denied',
		InvalidToken = 'invalid_token',
		Unauthorized = 'unauthorized'
	}
*/
var loginErrors = []string{"unauthorized", "invalid_token", "access_denied"}

type metricsService struct {
	central.UnimplementedMetricsServiceServer
}

func NewService() central.MetricsServiceServer {
	return &metricsService{}
}

func (m *metricsService) LoginMetrics(ctx context.Context, req *central.LoginMetricsReq) (*central.LoginMetricsResp, error) {
	tilog.Info(ctx, "start LoginMetrics", zap.Any("req", req.Email))

	if req.SyncCmClusterUser == "1" {
		go func() {
			_ = monitor.SyncClusterRelations(context.Background(), req.SyncCmClusterUserOrgId)
		}()
		return &central.LoginMetricsResp{}, nil
	}
	if req.SyncCmClusterUser == "2" {
		go func() {
			monitor.CreateHiddenProject(context.Background(), req.SyncCmClusterUserOrgId)
		}()
		return &central.LoginMetricsResp{}, nil
	}
	if req.SyncCmClusterUser == "3" {
		go func() {
			split := strings.Split(req.MonitorRoleBinding, ",")
			var pids []uint64
			for _, s := range split {
				pu, _ := strconv.ParseUint(s, 10, 64)
				pids = append(pids, pu)
			}
			monitor.ChangeProjectTypeFromDedicatedToTiDBX(context.Background(), pids)
		}()
		return &central.LoginMetricsResp{}, nil
	}

	//dbuser.RunDBUserClusterJob(ctx, dbuserSvc, userSvc)
	if req.EventType == LOGIN && req.EventResult == SUCCESS {
		user := getUserByToken(ctx, req.Token)

		//tilog.Info(ctx, "get value from viper ", zap.Any("server", viper.GetDuration("cache.expire")))

		if user != nil && user.ID != 0 {
			//return &central.LoginMetricsResp{}, nil
			tilog.Info(ctx, "GetUserFromContext user ", zap.Any("user", user.Email))
			checkDBUsers(ctx, user)
			//auto save access history
			go writeLoginHistory(context.Background(), user, req)
		}

	}
	if config.IsProdEnv() || req.EventType != "" {
		return &central.LoginMetricsResp{}, writeLoginMetrics(ctx, req)
	}

	if req.MultiRole == 1 {
		testMigrate(ctx, req)
		return &central.LoginMetricsResp{}, nil
	}

	return testGetDBUsers(ctx, req)

}

func writeLoginHistory(ctx context.Context, user *entity.User, req *central.LoginMetricsReq) {
	loginHistorySvc, _ := historysvc.GetHistorySvc()

	history := entity.EventHistory{
		IdentityType:  entity.IdentityTypeUser,
		IdentityID:    user.ID,
		IdentityValue: user.Email,
		Event:         entity.HistoryEventLogin,
	}
	err := loginHistorySvc.CreateEventHistory(ctx, &history)
	tilog.Info(ctx, "WriteUserLoginHistory user", zap.Any("user", user.ID.Uint64()))
	if err != nil {
		tilog.Error(ctx, "WriteUserLoginHistory fail ", zap.Error(err))

	}

}

func testMigrate(ctx context.Context, req *central.LoginMetricsReq) {
	if req.MultiRole == 1 {
		svc, _ := authcenter2.GetClusterUserSvc()
		credential := ""
		if req.Token != "" {
			decryptPasswd, err := authcenter2.DecryptCredential(ctx, req.Token)
			if err != nil {
				tilog.Error(ctx, "testMigrate CreateCloudAdmin decrypt req credential error", zap.Any("req", req), zap.Any("err", err))
				return
			}
			credential = decryptPasswd
		}
		param := authcenter2.CreateClusterUserParam{
			ClusterID:       req.ClusterId,
			ServerlessToken: "",
			Username:        authcenter2.CloudAdminUsername,
			CredentialType:  entity.CredentialTypePassword,
			Credential:      credential,
			TargetType:      entity.ClusterUserTargetBuiltinUser,
		}
		user, err := svc.CreateClusterUser(ctx, &param)
		tilog.Info(ctx, "testMigrate CreateClusterUser result", zap.Any("user", user), zap.Any("err", err))

		clusterUser, err := svc.GetClusterUser(ctx, req.ClusterId, authcenter2.CloudAdminUsername, 0)
		tilog.Info(ctx, "testMigrate GetClusterUser result", zap.Any("user", clusterUser), zap.Any("err", err))

		//handler := infra.GetHandler()
		//err := handler.CleanClusters(ctx, req.GetClusterId(), true)
		//tilog.Info(ctx, "testMigrate CleanClusters result", zap.Any("err", err))

		//cluster, err := handler.GetClusterById(ctx, req.ClusterId, true)
		//if err != nil || cluster == nil {
		//	tilog.Warn(ctx, "testMigrate GetClusterById error or cluster is nil", zap.Any("clusterId", req.ClusterId), zap.Any("err", err))
		//	return
		//}
		//cloudAdmin, err := infra.GetClusterCloudAdmin(ctx, cluster)
		//if err != nil {
		//	tilog.Warn(ctx, "testMigrate GetClusterCloudAdmin error", zap.Any("clusterId", req.ClusterId), zap.Any("err", err))
		//	return
		//}
		//tilog.Info(ctx, "testMigrate IsUserExistGrantRole start ...")
		//existing, err := infra.IsUserExistGrantRole(ctx, cluster, cloudAdmin, req.Email)
		//tilog.Warn(ctx, "testMigrate IsUserExistGrantRole result", zap.Any("clusterId", req.ClusterId), zap.Any("existing", existing), zap.Any("err", err))
		//
		//tilog.Info(ctx, "testMigrate IsUserExisting start ...")
		//existing, err = infra.IsUserExisting(ctx, cluster, cloudAdmin, req.Email)
		//tilog.Warn(ctx, "testMigrate IsUserExisting result", zap.Any("clusterId", req.ClusterId), zap.Any("existing", existing), zap.Any("err", err))
		//
		//params := infra.ListUsersParams{
		//	Cluster:  cluster,
		//	Operator: cloudAdmin,
		//	Filter: &core.ListDBUsersFilter{
		//		Users: []string{req.Email},
		//	},
		//}
		//tilog.Info(ctx, "testMigrate ListUsers start ...")
		//users, dbUsers, err := infra.ListUsers(ctx, &params)
		//tilog.Warn(ctx, "testMigrate ListUsers result", zap.Any("users", users), zap.Any("dbUsers", dbUsers), zap.Any("err", err))

	}
}

func testGetDBUsers(ctx context.Context, req *central.LoginMetricsReq) (*central.LoginMetricsResp, error) {
	tilog.Info(ctx, "get jwt req", zap.Any("req", req))
	client := utils.GetAccountAclClient()
	response := central.LoginMetricsResp{}
	if req.AccessKey != "" {
		rpcReq := acl.GetJwtByApiKeyReq{
			ClusterId: req.ClusterId,
			AccessKey: req.AccessKey,
		}
		resp, err := client.GetJwtByApiKey(ctx, &rpcReq)
		tilog.Info(ctx, "call acl GetJwtByApiKey result", zap.Any("req", req), zap.Any("resp", resp))
		if err != nil {
			tilog.Error(ctx, "call acl GetJwtByApiKey error, this api is test ,please ignore", zap.Error(err))
		}
		if resp != nil {
			response.BaseResp = resp.BaseResp
			response.DbUser = resp.DbUser
			response.JwtToken = resp.Jwt
		}

		return &response, nil
	}
	if req.Email != "" || req.UserId != 0 {
		rpcReq := acl.GetJwtByUserReq{
			ClusterId: req.ClusterId,
			Email:     req.Email,
			UserId:    req.UserId,
		}
		resp, err := client.GetJwtByUser(ctx, &rpcReq)
		tilog.Info(ctx, "call acl GetJwtByUser result", zap.Any("req", req), zap.Any("resp", resp))
		if err != nil {
			tilog.Error(ctx, "call acl GetJwtByUser error, this api is test ,please ignore", zap.Error(err))
		}
		if resp != nil {
			response.BaseResp = resp.BaseResp
			response.DbUser = resp.DbUser
			response.JwtToken = resp.Jwt
		}

		return &response, nil
	} else if req.Token != "" {
		rpcReq := acl.GetJwtByTokenReq{
			ClusterId:  req.ClusterId,
			Auth0Token: req.Token,
		}
		resp, err := client.GetJwtByToken(ctx, &rpcReq)
		tilog.Info(ctx, "call acl GetJwtByToken result", zap.Any("req", req), zap.Any("resp", resp))
		if err != nil {
			tilog.Error(ctx, "call acl GetJwtByToken error,this api is test ,please ignore", zap.Error(err))
		}

		if resp != nil {
			response.BaseResp = resp.BaseResp
			response.DbUser = resp.DbUser
			response.JwtToken = resp.Jwt
		}
	}

	return &response, nil
}

func checkDBUsers(ctx context.Context, user *entity.User) {

	if user.Email != "" {

		bm := utils.Begin(ctx, "checkDBUsers", zap.Any("user", user.Email))
		defer bm.OnExit(ctx)
		svc, _ := rbacsvc.GetRbacRoleBindingSvc()
		dbUserSvc, _ := dbuser.GetDBUserSvc()
		userSvc, _ := user2.GetUserSvc()

		orgIds, _ := svc.GetUserOrgIds(ctx, user.ID.Uint64())
		for _, orgId := range orgIds {

			var opid uint64 = 0
			ownerIds, err := svc.ListUserIdByRoles(ctx, entity.RoleBindingScopeOrg, orgId, []string{entity.RoleOrgOwner})
			tilog.Info(ctx, "ListUserIdByRoles result ", zap.Any("ownerIds", ownerIds))
			if err != nil {
				tilog.Info(ctx, "checkDBUsers invoke ListUserIdByRoles fail", zap.Error(err))
				return
			}

			var operatorIds []uint64
			for _, ownerId := range ownerIds {
				if ownerId.Uint64() != user.ID.Uint64() {
					u, err := userSvc.GetUserByID(ctx, ownerId.Uint64())
					if err != nil {
						tilog.Info(ctx, "GetUserByID fail ", zap.Any("userId", ownerId.Uint64()), zap.Error(err))
						continue
					}
					if u.Status == entity.UserStatusActive {
						operatorIds = append(operatorIds, ownerId.Uint64())
					}
				}
			}
			if len(operatorIds) > 0 {
				rand.Seed(time.Now().UnixNano())
				randomIndex := rand.Intn(len(operatorIds))
				tilog.Info(ctx, "randomIndex", zap.Any("randomIndex", randomIndex))
				opid = operatorIds[randomIndex]
			}
			// operator is exist
			if opid != 0 {
				tilog.Info(ctx, "save check command for user "+user.Email, zap.Any("operatorId", opid), zap.Any("orgId", orgId))
				if err := dbUserSvc.CreateCommandUserAction(ctx, opid, user.ID.Uint64(), user.Email, orgId, 0, entity.CommandStatusWaiting, entity.CommandActionCheck, entity.RoleBindingTargetUser, entity.RoleBindingTargetUser); err != nil {
					tilog.Error(ctx, "CreateCommandUserAction fail", zap.Error(err))
				}
			}

		}
		tilog.Info(ctx, "checkDBUsers finish", zap.Any("user", user.Email))
	}

}

func writeLoginMetrics(ctx context.Context, req *central.LoginMetricsReq) error {

	myLimiterObj := singleton.GetLimiterSingleton()
	if !myLimiterObj.Limiter.Allow() {
		tilog.Info(ctx, "request login-metrics to many, hit limiter")
		errMsg := errors.New("request api <login-metrics> limit")
		err := errcode.NewNotAuthenticatedErr(errMsg)
		return err
	}

	err := checkParam(req)
	if err != nil {
		return err
	}

	eventType := req.GetEventType()
	eventResult := req.GetEventResult()
	cost := req.GetCost()
	errorMsg := req.GetError()
	desc := req.GetDesc()
	eventTimeStr := req.GetEventTime()
	loginType := ""
	email := ""
	timeIntVal, _ := strconv.ParseInt(eventTimeStr, 10, 64)
	eventTime := time.Unix(timeIntVal, 0).Format("2006-01-02 15:04:05")

	//login success write  metric cost
	if eventType == LOGIN && eventResult == SUCCESS {
		//authSub := ctx.Value("AUTH_SUB")
		//loginType = authSub.(string)

		user := getUserByToken(ctx, req.Token)
		if user == nil {
			return nil

		}

		email = user.Email
		uid := user.ID.Uint64()
		costVal, _ := strconv.ParseFloat(cost, 64)

		//costVal = costVal * 1000
		if costVal > 0 {
			metrics.LoginCostSummaryDuration.Observe(costVal)
			metrics.LoginCostGaugeDuration.Set(costVal)
			tilog.Info(ctx, "writeLoginMetrics success", zap.String("email", email), zap.String("eventCost", cost))
			if costVal > 10 {
				tilog.Warn(ctx, "login eventCost more than 10s", zap.String("email", email), zap.String("eventCost", cost))
			}
		} else {
			//add log record
			tilog.Warn(ctx, "login eventCost is wrong  value:", zap.String("email", email), zap.String("eventCost", cost))
		}
		// send login event msg to SQS
		event := loginEvent{
			Uid:       uid,
			LoginDate: eventTime,
			MsgType:   "login",
		}
		SendToUserEventSQS(event)
		// test reveive
		// ReceiveMsg()
	}

	tilog.Info(ctx, "receive login event:",
		zap.String("loginType", loginType),
		zap.String("eventResult", eventResult),
		zap.String("email", email),
		zap.String("eventType", eventType),
		zap.String("error", errorMsg),
		zap.String("code", desc),
		zap.String("eventCost", cost),
		zap.String("eventTime", eventTime))
	// skip the some error
	for _, value := range loginErrors {
		if value == errorMsg {
			return err
		}

	}
	// total login login event
	metrics.LoginCounterDuration.WithLabelValues(eventResult).Inc()

	return err
}

func checkParam(req *central.LoginMetricsReq) error {
	errMsg := ""
	if req.GetEventType() == "" {
		errMsg = "event_type must not null "
	}
	if req.GetEventTime() == "" {
		errMsg = errMsg + "event_time must not null "
	}
	if req.GetEventResult() == "" {
		errMsg = errMsg + "event_result must not null "
	}
	if req.GetEventResult() == "SUCCESS" && req.GetCost() == "" {
		errMsg = errMsg + "event_cost must not null "
	}
	if req.GetEventTime() != "" {
		timeIntVal, _ := strconv.ParseInt(req.GetEventTime(), 10, 64)
		eventTime := time.Unix(timeIntVal, 0)
		diff := time.Since(eventTime).Seconds()
		if diff > 60 {
			tilog.PlainInfo("eventTime diff", zap.Any("diff", diff))
			errMsg = errMsg + "event_time is wrong "
		}

	}

	if errMsg == "" {
		return nil
	}
	return errcode.NewInvalidInputErr(errors.New(errMsg))
}

func getUserByToken(ctx context.Context, token string) *entity.User {

	if token == "" {
		tilog.Info(ctx, "token is nil")
		return nil
	}
	tokenSvc, err := tokensvc.GetTokenSvc()
	if err != nil {
		tilog.Error(ctx, "GetTokenSvc error", zap.Error(err))
		return nil
	}
	userSvc, _ := user2.GetUserSvc()
	email, tokenType, err := tokenSvc.GetEmailTokenTypeByToken(ctx, token)
	if err != nil {
		tilog.Info(ctx, "GetEmailTokenTypeByToken error", zap.Error(err))
		return nil
	}
	tilog.Info(ctx, "get user by email", zap.Any("email", email), zap.Any("tokenType", tokenType))

	user, err := userSvc.GetUserByEmail(ctx, email)
	if err != nil {
		tilog.Info(ctx, "failed to get user by email", zap.Error(err))
	}
	return user

}
