package auth

import (
	"context"
	"encoding/base64"
	"fmt"
	"strings"

	"github.com/tidbcloud/account/central/utils"

	"github.com/AlekSi/pointer"
	"github.com/skip2/go-qrcode"
	"github.com/tidbcloud/account/internel/dao/entity"
	"github.com/tidbcloud/account/internel/svc/authn"
	"github.com/tidbcloud/account/thirdparty/auth0"
	"github.com/tidbcloud/tiinf/errs"

	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/idl/pbgen/proto/central"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/emptypb"
	"gopkg.in/auth0.v4/management"
)

const (
	alreadyEnrolledErrMsg = "already enrolled"
)

type authService struct {
	central.UnimplementedAuthServiceServer
	authn.OperationsService
	management *management.Management
}

var _ central.AuthServiceServer = (*authService)(nil)

// NewService constructs an AuthServiceServer
func NewService(s authn.OperationsService, management *management.Management) central.AuthServiceServer {
	return &authService{OperationsService: s, management: management}
}

func (s *authService) Meta(ctx context.Context, _ *emptypb.Empty) (*central.MetaData, error) {
	user := s.CurrentUser(ctx)
	email := user.Email
	users, err := s.management.User.ListByEmail(email)
	if err != nil || len(users) < 1 {
		tilog.Warn(ctx, "failed to list users by email in Auth0", zap.String("email", email), zap.Error(err))
		return nil, utils.ConvertErr2GrpcErr(errs.NewRequestTimeout(fmt.Sprintf("failed to list users by email[%s] in Auth0", email), err))
	}
	var auth0User *management.User = nil
	auth0UserID := s.CurrentUserSub(ctx)
	for _, u := range users {
		userID := pointer.GetString(u.ID)
		if userID == auth0UserID {
			auth0User = u
			break
		}
	}
	if auth0User == nil {
		tilog.Warn(ctx, "not found current user by email in Auth0", zap.String("email", email), zap.Any("auth0UserID", auth0UserID))
		return nil, utils.ConvertErr2GrpcErr(errs.NewNotFound(fmt.Sprintf("not found current user by email[%s] in Auth0", email), nil))
	}

	useMfa := false
	val, ok := auth0User.UserMetadata["use_mfa"]
	if ok && val.(bool) {
		useMfa = true
	}
	return &central.MetaData{
		Email:    email,
		TenantId: user.TenantID.Uint64(),
		UseMfa:   useMfa,
	}, nil
}

func (s *authService) UpdateMeta(ctx context.Context, req *central.UpdateMetaReq) (*emptypb.Empty, error) {
	if req.UseMfa {
		return nil, errs.NewBadRequest("only for closing meta use_mfa", nil)
	}
	user := s.CurrentUser(ctx)
	email := user.Email
	users, err := s.management.User.ListByEmail(email)
	if err != nil || len(users) < 1 {
		tilog.Error(ctx, "failed to list users by email in Auth0", zap.String("email", email), zap.Error(err))
		return &emptypb.Empty{}, errors.Errorf("failed to list users by email[%s] in Auth0", email)
	}
	auth0UserID := s.CurrentUserSub(ctx)
	for _, u := range users {
		userID := pointer.GetString(u.ID)
		if userID != auth0UserID {
			continue
		}
		tilog.Info(ctx, "update user meta start", zap.String("email", email), zap.Any("id", u.ID))
		if !req.UseMfa {
			err := auth0.DeleteMfaMethods(ctx, *u.ID)
			if err != nil {
				tilog.Error(ctx, "update user meta, delete mfa methods error", zap.String("email", email), zap.Any("id", u.ID), zap.Error(err))
			} else {
				tilog.Info(ctx, "update user meta, delete mfa methods success", zap.String("email", email), zap.Any("id", u.ID))
			}
		}
		updateUser := &management.User{
			UserMetadata: u.UserMetadata,
		}
		if updateUser.UserMetadata == nil {
			updateUser.UserMetadata = make(map[string]interface{})
		}
		updateUser.UserMetadata["use_mfa"] = req.UseMfa
		err := s.management.User.Update(*u.ID, updateUser)
		tilog.Info(ctx, "update user meta, update use_mfa result", zap.String("email", email), zap.Any("id", u.ID), zap.Error(err))
		action := "disable"
		if req.UseMfa {
			action = "enable"
		}
		auditlog.NewEvent(ctx, auditlog.EventSetMFA).SetResult(err == nil).SetDetails(auditlog.Details{
			auditlog.KeyEnableOrDisable: action,
			auditlog.KeyError:           err,
		}.Build()).Submit() //nolint:errcheck
		if err != nil {
			tilog.Error(ctx, "failed to update user metadata in Auth0", zap.String("email", email), zap.Error(err))
			return &emptypb.Empty{}, errors.Errorf("failed to update user metadata of %s in Auth0", email)
		}
	}
	return &emptypb.Empty{}, nil
}

func (s *authService) VerifyEmailPassword(ctx context.Context, req *central.VerifyEmailPasswordReq) (*central.VerifyEmailPasswordRsp, error) {
	if req == nil || req.Password == "" {
		return nil, errs.NewBadRequest("param is invalid", nil)
	}
	user := s.CurrentUser(ctx)
	email := user.Email
	ret, err := auth0.VerifyEmailPassword(ctx, email, req.Password)
	if err != nil {
		return nil, err
	}
	centralRet := &central.VerifyEmailPasswordRsp{
		Ok:  ret.OK,
		Msg: auth0.VerifySuccessMsg,
	}
	if ret.OK {
		return centralRet, nil
	}
	if ret.ErrorRet != nil && ret.ErrorRet.Error == auth0.MfaRequiredError {
		centralRet.Msg = auth0.VerifyFailMsg
		centralRet.MfaToken = ret.ErrorRet.MfaToken
		return centralRet, nil
	}
	return nil, errs.NewBadRequest(auth0.VerifyFailMsg, nil)
}

func (s *authService) VerifyOtpCode(ctx context.Context, req *central.VerifyOtpCodeReq) (*central.VerifyRsp, error) {
	if req == nil || req.MfaToken == "" || req.OtpCode == "" {
		return nil, errs.NewBadRequest("param is invalid", nil)
	}
	ret, err := auth0.VerifyOtpCode(ctx, req.MfaToken, req.OtpCode)
	if err != nil {
		return nil, err
	}
	if ret.OK {
		return &central.VerifyRsp{
			Ok:  ret.OK,
			Msg: ret.Msg,
		}, nil
	}
	return nil, errs.NewBadRequest(ret.Msg, nil)
}

func (s *authService) VerifyRecoveryCode(ctx context.Context, req *central.VerifyRecoveryCodeReq) (*central.VerifyRsp, error) {
	if req == nil || req.MfaToken == "" || req.RecoveryCode == "" {
		return nil, errs.NewBadRequest("param is invalid", nil)
	}
	ret, err := auth0.VerifyRecoveryCode(ctx, req.MfaToken, req.RecoveryCode)
	if err != nil {
		return nil, err
	}
	recoveryCode := ""
	if ret.AccessTokenRet != nil {
		recoveryCode = ret.AccessTokenRet.RecoveryCode
	}
	if ret.OK {
		return &central.VerifyRsp{
			Ok:           ret.OK,
			Msg:          ret.Msg,
			RecoveryCode: recoveryCode,
		}, nil
	}
	return nil, errs.NewBadRequest(ret.Msg, nil)
}

func (s *authService) StartEnableMFA(ctx context.Context, req *central.StartEnableMFAReq) (*central.StartEnableMFARsp, error) {
	if req == nil || req.Password == "" {
		return nil, errs.NewBadRequest("param is invalid", nil)
	}
	user := s.CurrentUser(ctx)
	email := user.Email
	auth0UserID := s.CurrentUserSub(ctx)
	tilog.Info(ctx, "StartEnableMFA start", zap.String("email", email), zap.Any("auth0UserID", auth0UserID))

	// 1. verify email password
	verifyPasswordRet, err := auth0.VerifyEmailPassword(ctx, email, req.Password)
	if err != nil {
		tilog.Error(ctx, "StartEnableMFA VerifyEmailPassword error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
		return nil, err
	}
	if !verifyPasswordRet.OK {
		tilog.Info(ctx, "StartEnableMFA VerifyEmailPassword fail", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("errMsg", verifyPasswordRet.Msg))
		return nil, errs.NewBadRequest(verifyPasswordRet.Msg, nil)
	}
	tilog.Info(ctx, "StartEnableMFA VerifyEmailPassword success", zap.String("email", email), zap.Any("auth0UserID", auth0UserID))
	// 2. open user meta use_mfa
	err = s.updateCurrentUserAuth0MetadataUseMfa(ctx, email, auth0UserID, true)
	if err != nil {
		tilog.Info(ctx, "StartEnableMFA updateCurrentUserAuth0MetadataUseMfa to true error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
		return nil, errs.NewInternalUnknown("update user meta error", err)
	}
	tilog.Info(ctx, "StartEnableMFA updateCurrentUserAuth0MetadataUseMfa to true success", zap.String("email", email), zap.Any("auth0UserID", auth0UserID))
	// finally, close user meta use_mfa
	defer func() {
		err := s.updateCurrentUserAuth0MetadataUseMfa(ctx, email, auth0UserID, false)
		if err != nil {
			tilog.Error(ctx, "StartEnableMFA defer updateCurrentUserAuth0MetadataUseMfa to false error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
		} else {
			tilog.Info(ctx, "StartEnableMFA defer updateCurrentUserAuth0MetadataUseMfa to false success", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
		}
	}()
	// 3. verify email password to get mfa_token
	verifyPasswordRet, err = auth0.VerifyEmailPassword(ctx, email, req.Password)
	if err != nil {
		tilog.Error(ctx, "StartEnableMFA VerifyEmailPassword to get mfa token error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
		return nil, err
	}
	if verifyPasswordRet.ErrorRet == nil || verifyPasswordRet.ErrorRet.Error != auth0.MfaRequiredError {
		tilog.Error(ctx, "StartEnableMFA VerifyEmailPassword to get mfa token fail", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("verifyPasswordRet", verifyPasswordRet))
		return nil, errs.NewInternalUnknown("get mfa token from auth0 fail", nil)
	}
	tilog.Info(ctx, "StartEnableMFA VerifyEmailPassword to get mfa token success", zap.String("email", email), zap.Any("auth0UserID", auth0UserID))
	// 4. enroll otp authenticator
	enrollRet, err := auth0.EnrollOtpAuthenticator(ctx, verifyPasswordRet.ErrorRet.MfaToken)
	if err != nil {
		tilog.Warn(ctx, "StartEnableMFA EnrollOtpAuthenticator error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.String("mfaToken", verifyPasswordRet.ErrorRet.MfaToken), zap.Any("err", err))
		if strings.Contains(err.Error(), alreadyEnrolledErrMsg) {
			return nil, errs.NewBadRequest("MFA is already enabled.", err)
		}
		return nil, errs.NewInternalUnknown("enroll opt authenticator on auth0 error", err)
	}
	base64Qrcode, err := generateQRCode(ctx, enrollRet.BarcodeUri)
	if err != nil {
		tilog.Error(ctx, "StartEnableMFA EnrollOtpAuthenticator generateQRCode error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.String("barcodeUri", enrollRet.BarcodeUri), zap.Any("err", err))
		return nil, errs.NewInternalUnknown("generate QRCode error", err)
	}
	tilog.Info(ctx, "StartEnableMFA EnrollOtpAuthenticator success", zap.String("email", email), zap.Any("auth0UserID", auth0UserID))

	recoveryCode := ""
	if len(enrollRet.RecoveryCodes) > 0 {
		recoveryCode = enrollRet.RecoveryCodes[0]
	} else {
		recoveryCode, err = auth0.RegenerateRecoveryCode(ctx, auth0UserID)
		if err != nil {
			tilog.Error(ctx, "StartEnableMFA EnrollOtpAuthenticator RegenerateRecoveryCode error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
			return nil, errs.NewInternalUnknown("regenerate recovery code error", err)
		}
	}
	rsp := &central.StartEnableMFARsp{
		AuthenticatorType: enrollRet.AuthenticatorType,
		Secret:            enrollRet.Secret,
		BarcodeUri:        enrollRet.BarcodeUri,
		RecoveryCode:      recoveryCode,
		Base64Qrcode:      base64Qrcode,
	}
	tilog.Info(ctx, "StartEnableMFA result", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("rsp", rsp))

	return rsp, nil
}

func (s *authService) ConfirmMfaAuthenticators(ctx context.Context, req *central.ConfirmMfaAuthenticatorsReq) (*central.VerifyRsp, error) {
	if req == nil || req.Password == "" || req.OtpCode == "" {
		return nil, errs.NewBadRequest("param is invalid", nil)
	}
	user := s.CurrentUser(ctx)
	email := user.Email
	auth0UserID := s.CurrentUserSub(ctx)
	tilog.Info(ctx, "ConfirmMfaAuthenticators start", zap.String("email", email), zap.Any("auth0UserID", auth0UserID))
	// 1. open user meta use_mfa
	err := s.updateCurrentUserAuth0MetadataUseMfa(ctx, email, auth0UserID, true)
	if err != nil {
		tilog.Info(ctx, "ConfirmMfaAuthenticators updateCurrentUserAuth0MetadataUseMfa to true error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
		return nil, errs.NewInternalUnknown("update user meta error", err)
	}
	tilog.Info(ctx, "ConfirmMfaAuthenticators updateCurrentUserAuth0MetadataUseMfa to true success", zap.String("email", email), zap.Any("auth0UserID", auth0UserID))

	var confirmOK = true
	defer func() {
		if !confirmOK {
			err := s.updateCurrentUserAuth0MetadataUseMfa(ctx, email, auth0UserID, false)
			if err != nil {
				tilog.Error(ctx, "ConfirmMfaAuthenticators defer updateCurrentUserAuth0MetadataUseMfa to false error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
			} else {
				tilog.Info(ctx, "ConfirmMfaAuthenticators defer updateCurrentUserAuth0MetadataUseMfa to false success", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
			}
		}
	}()

	// 2. verify email password to get mfa_token
	verifyPasswordRet, err := auth0.VerifyEmailPassword(ctx, email, req.Password)
	if err != nil {
		confirmOK = false
		tilog.Error(ctx, "ConfirmMfaAuthenticators VerifyEmailPassword to get mfa token error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
		return nil, err
	}
	if verifyPasswordRet.ErrorRet == nil || verifyPasswordRet.ErrorRet.Error != auth0.MfaRequiredError {
		confirmOK = false
		tilog.Warn(ctx, "ConfirmMfaAuthenticators VerifyEmailPassword to get mfa token fail", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("verifyPasswordRet", verifyPasswordRet))
		return nil, errs.NewInternalUnknown("get mfa token from auth0 fail", nil)
	}
	// 3. verify otp code
	verifyOtpCodeRet, err := auth0.VerifyOtpCode(ctx, verifyPasswordRet.ErrorRet.MfaToken, req.OtpCode)
	if err != nil {
		confirmOK = false
		tilog.Error(ctx, "ConfirmMfaAuthenticators VerifyOtpCode error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("mfaToken", verifyPasswordRet.ErrorRet.MfaToken), zap.Any("optCode", req.OtpCode), zap.Any("err", err))
		return nil, errs.NewInternalUnknown("verify otp code error", err)
	}
	if !verifyOtpCodeRet.OK {
		confirmOK = false
		tilog.Info(ctx, "ConfirmMfaAuthenticators VerifyOtpCode fail", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("verifyOtpCodeRet", verifyOtpCodeRet))
		return nil, errs.NewBadRequest(verifyOtpCodeRet.Msg, nil)
	}
	tilog.Info(ctx, "ConfirmMfaAuthenticators VerifyOtpCode success", zap.String("email", email), zap.Any("auth0UserID", auth0UserID))

	auditlog.NewEvent(ctx, auditlog.EventSetMFA).SetResult(true).SetDetails(auditlog.Details{
		auditlog.KeyEnableOrDisable: "enable",
		auditlog.KeyError:           nil,
	}.Build()).Submit() //nolint:errcheck

	centralRet := &central.VerifyRsp{
		Ok:  verifyOtpCodeRet.OK,
		Msg: verifyOtpCodeRet.Msg,
	}
	return centralRet, nil
}

func (s *authService) updateCurrentUserAuth0MetadataUseMfa(ctx context.Context, email, auth0UserID string, useMfa bool) error {
	users, err := s.management.User.ListByEmail(email)
	if err != nil {
		tilog.Error(ctx, "failed to list users by email in Auth0", zap.String("email", email), zap.Error(err))
		return errs.NewInternalUnknown("failed to list users by email in Auth0", err)
	}
	if len(users) < 1 {
		tilog.Info(ctx, "not found users by email in Auth0", zap.String("email", email), zap.Error(err))
		return errs.NewBadRequest("not found users by email in Auth0", nil)
	}
	for _, u := range users {
		userID := pointer.GetString(u.ID)
		if userID != auth0UserID {
			tilog.Info(ctx, "update user meta, not current user, ignore", zap.String("email", email), zap.Any("id", u.ID))
			continue
		}
		tilog.Info(ctx, "update user meta start", zap.String("email", email), zap.Any("id", u.ID), zap.Any("useMfa", useMfa))
		updateUser := &management.User{
			UserMetadata: u.UserMetadata,
		}
		if updateUser.UserMetadata == nil {
			updateUser.UserMetadata = make(map[string]interface{})
		}
		updateUser.UserMetadata["use_mfa"] = useMfa
		err := s.management.User.Update(*u.ID, updateUser)
		if err != nil {
			tilog.Error(ctx, "update user meta, update use_mfa error", zap.String("email", email), zap.Any("id", u.ID), zap.Any("useMfa", useMfa), zap.Error(err))
			return err
		}
		tilog.Info(ctx, "update user meta, update use_mfa success", zap.String("email", email), zap.Any("id", u.ID), zap.Any("useMfa", useMfa), zap.Error(err))

	}
	return nil
}

func generateQRCode(ctx context.Context, barcodeUri string) (string, error) {
	if barcodeUri == "" {
		return "", nil
	}
	qrBytes, err := qrcode.Encode(barcodeUri, qrcode.Medium, 126)
	if err != nil {
		tilog.Error(ctx, "generateQRCode error", zap.Any("barcodeUri", barcodeUri), zap.Any("err", err))
		return "", err
	}
	base64QR := base64.StdEncoding.EncodeToString(qrBytes)
	return base64QR, nil
}

func (s *authService) DisableMFA(ctx context.Context, req *central.DisableMfaReq) (*emptypb.Empty, error) {
	auth0UserID := s.CurrentUserSub(ctx)
	var emailPasswordLoginUser = false
	if strings.HasPrefix(auth0UserID, string(entity.AuthMethodNameEmail)) {
		if req.Password == "" {
			return nil, errs.NewBadRequest("param is invalid", nil)
		}
		if req.OtpCode == "" && req.RecoveryCode == "" {
			return nil, errs.NewBadRequest("param is invalid", nil)
		}
		emailPasswordLoginUser = true
	}
	tilog.Info(ctx, "DisableMFA request", zap.Any("req", req), zap.Any("auth0UserID", auth0UserID))
	if !emailPasswordLoginUser {
		return s.UpdateMeta(ctx, &central.UpdateMetaReq{
			UseMfa: false,
		})
	}
	// for email/password login user to check otp code
	user := s.CurrentUser(ctx)
	email := user.Email
	// 1. verify email password to get mfa_token
	verifyPasswordRet, err := auth0.VerifyEmailPassword(ctx, email, req.Password)
	if err != nil {
		tilog.Error(ctx, "DisableMFA VerifyEmailPassword to get mfa token error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("err", err))
		return nil, err
	}
	if verifyPasswordRet.ErrorRet == nil || verifyPasswordRet.ErrorRet.Error != auth0.MfaRequiredError {
		tilog.Warn(ctx, "DisableMFA VerifyEmailPassword to get mfa token fail", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("verifyPasswordRet", verifyPasswordRet))
		return nil, errs.NewBadRequest("get mfa token from auth0 fail", nil)
	}
	if req.OtpCode != "" {
		// 2.1 verify otp code
		verifyOtpCodeRet, err := auth0.VerifyOtpCode(ctx, verifyPasswordRet.ErrorRet.MfaToken, req.OtpCode)
		if err != nil {
			tilog.Error(ctx, "DisableMFA VerifyOtpCode error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("mfaToken", verifyPasswordRet.ErrorRet.MfaToken), zap.Any("optCode", req.OtpCode), zap.Any("err", err))
			return nil, errs.NewInternalUnknown("verify otp code error", err)
		}
		if !verifyOtpCodeRet.OK {
			tilog.Info(ctx, "DisableMFA VerifyOtpCode fail", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("verifyOtpCodeRet", verifyOtpCodeRet))
			return nil, errs.NewBadRequest(verifyOtpCodeRet.Msg, nil)
		}
	} else {
		// 2.2 verify recovery code
		ret, err := auth0.VerifyRecoveryCode(ctx, verifyPasswordRet.ErrorRet.MfaToken, req.RecoveryCode)
		if err != nil {
			tilog.Error(ctx, "DisableMFA VerifyRecoveryCode error", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("mfaToken", verifyPasswordRet.ErrorRet.MfaToken), zap.Any("recoveryCode", req.RecoveryCode), zap.Any("err", err))
			return nil, err
		}
		if !ret.OK {
			tilog.Info(ctx, "DisableMFA VerifyRecoveryCode fail", zap.String("email", email), zap.Any("auth0UserID", auth0UserID), zap.Any("verifyRecoveryCodeRet", ret))
			return nil, errs.NewBadRequest(ret.Msg, nil)
		}
	}

	tilog.Info(ctx, "DisableMFA verify otpCode/recoveryCode success", zap.String("email", email), zap.Any("auth0UserID", auth0UserID))
	return s.UpdateMeta(ctx, &central.UpdateMetaReq{
		UseMfa: false,
	})
}
