package tenant

import (
	"context"
	"net/http"
	"strings"
	"time"

	"github.com/gogo/protobuf/types"
	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db/entity"
	tenantCentral "github.com/pingcap/dbaas/central/db/tenant"
	"github.com/pingcap/dbaas/central/model"
	"github.com/pingcap/dbaas/central/model/admin"
	"github.com/pingcap/dbaas/central/model/tenant"
	"github.com/pingcap/dbaas/central/service"
	"github.com/pingcap/dbaas/central/singleton"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/idl/pbgen/proto/central"
	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/http/recaptcha"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type tenantService struct {
	central.UnimplementedTenantServiceServer
	service.OperationsService
	tenantModel tenant.Model
	adminModel  admin.Model
}

var _ central.TenantServiceServer = (*tenantService)(nil)

// NewService constructs a tenantService
func NewService(s service.OperationsService, model tenant.Model, adminModel admin.Model) central.TenantServiceServer {
	return &tenantService{OperationsService: s, tenantModel: model, adminModel: adminModel}
}

func (s *tenantService) userContext(ctx context.Context) tenant.Context {
	return s.tenantModel.User(ctx, s.CurrentUser(ctx))
}

func (s *tenantService) Log(ctx context.Context) *zap.Logger {
	return s.userContext(ctx).Log()
}

func getAuth0ConnectionType(c central.RegistrationReq_ConnectionType) (config.Auth0ConnectionType, error) {
	switch c {
	case central.RegistrationReq_GOOGLE:
		return config.Auth0SocialConnectionGoogle, nil
	case central.RegistrationReq_GITHUB:
		return config.Auth0SocialConnectionGithub, nil
	case central.RegistrationReq_EMAIL:
		return config.Auth0DatabaseConnection, nil
	default:
		return "", errors.Errorf("unknown connection type: %v", c)
	}
}

func buildCreateOrgParams(req *central.RegistrationReq, clientIP string, connectionType config.Auth0ConnectionType) *tenant.CreateOrgParams {
	return &tenant.CreateOrgParams{
		Name:                      req.Company,
		Email:                     req.Email,
		Country:                   req.Country,
		Password:                  req.Password,
		FirstName:                 req.FirstName,
		LastName:                  req.LastName,
		Job:                       req.Job,
		PhoneNumber:               req.PhoneNumber,
		ConnectionType:            connectionType,
		ClientIP:                  clientIP,
		Auth0Token:                req.Auth0Token,
		ThirdPartyAccountProvider: req.ThirdPartyAccountProvider,
		ThirdPartyAccountToken:    req.ThirdPartyAccountToken,
	}
}

func (s *tenantService) Registration(ctx context.Context, req *central.RegistrationReq) (*central.DefaultResp, error) {
	sanitizedRequest := &central.RegistrationReq{
		Country:   s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.Country)),
		FirstName: s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.FirstName)),
		LastName:  s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.LastName)),
		// auth0 use lowercase email
		Email:                     s.SanitizerPolicy().Sanitize(strings.ToLower(strings.TrimSpace(req.Email))),
		Job:                       s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.Job)),
		Company:                   s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.Company)),
		PhoneNumber:               s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.PhoneNumber)),
		Password:                  req.Password,
		AcceptService:             req.AcceptService,
		RecaptchaResponse:         s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.RecaptchaResponse)),
		ThirdPartyAccountProvider: s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.ThirdPartyAccountProvider)),
		ThirdPartyAccountToken:    s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.ThirdPartyAccountToken)),
		ConnectionType:            req.ConnectionType,
		Auth0Token:                req.Auth0Token,
	}

	connectionType, err := getAuth0ConnectionType(req.ConnectionType)
	if err != nil {
		log.Error(ctx, "failed to get auth0 connection type", zap.String("feature", "signup"), zap.Error(err))
		return nil, bizerr.New(bizerr.ErrCodeParams, "failed to get auth0 connection type")
	}

	err = validateRegistrationReq(sanitizedRequest, connectionType.IsAuth0SocialConnection())
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, err.Error())
	}

	if config.Global().ServerConfig.EnableRecaptcha && req.ConnectionType == central.RegistrationReq_EMAIL {
		// send google response to https://www.google.com/recaptcha/api/siteverify
		// to check out weather user has passed the recaptcha verification.
		recaptchaHandle := recaptcha.NewRecaptchaHandle(&http.Client{Timeout: 5 * time.Second}, config.Global().ServerConfig.RecaptchaSecret)
		if _, err := recaptchaHandle.ValidateReCAPTCHA(req.RecaptchaResponse); err != nil {
			log.Error(ctx, "failed to validate recaptcha", zap.String("feature", "signup"), zap.Error(err))
			return nil, bizerr.New(bizerr.ErrCodeUnknown, err.Error())
		}
	}

	clientIP, err := service.GetClientIP(ctx)
	if err != nil {
		log.Error(ctx, "failed to get client IP address", zap.String("feature", "signup"), zap.Error(err))
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "failed to get client IP address")
	}

	createOrgParams := buildCreateOrgParams(sanitizedRequest, clientIP, connectionType)
	userContext := s.tenantModel.User(ctx, &tenantCentral.User{})
	if err := userContext.CreateOrg(ctx, createOrgParams); err != nil {
		return nil, err
	}

	resp := &central.DefaultResp{
		BaseResp: &central.BaseResp{},
	}
	return resp, nil
}

func (s *tenantService) SendVerificationEmail(ctx context.Context, req *central.SendVerificationEmailReq) (*types.Empty, error) {
	email := s.SanitizerPolicy().Sanitize(strings.ToLower(strings.TrimSpace(req.Email)))

	// limitation of send verification-email
	accessor := email
	target := email
	rateConf := config.Global().RateControlConfig.RegistrationVerification
	now := time.Now()
	rateController := singleton.RateControllerManager.WithScene(rateConf.Scene)
	if !rateController.AllowAll(ctx, accessor, target, now, rateConf.Rates...) {
		return nil, errcode.NewInvalidInputErr(errors.New("request send verification email too often"))
	}
	rateController.Access(ctx, accessor, target, now)

	userContext := s.tenantModel.User(ctx, &tenantCentral.User{})
	if err := userContext.SendVerificationEmail(ctx, email); err != nil {
		return &types.Empty{}, err
	}
	return &types.Empty{}, nil
}

func (s *tenantService) GetOrganization(ctx context.Context, _ *types.Empty) (*central.Organization, error) {
	organization, err := s.userContext(ctx).GetOrganization()
	if err != nil {
		logger := s.Log(ctx)
		logger.Error("failed to get organization", zap.Error(err))
		if errors.Find(err, func(e error) bool { return e == gorm.ErrRecordNotFound }) != nil {
			return nil, errcode.NewNotFoundErr(errors.New("failed to get organization"))
		}
		return nil, errcode.NewInternalErr(errors.New("failed to get organization"))
	}
	return s.organizationOutput(organization)
}

func (s *tenantService) organizationOutput(org *tenant.Organization) (*central.Organization, error) {
	createdAt, err := types.TimestampProto(org.CreateAt)
	if err != nil {
		return nil, errcode.NewInternalErr(errors.New("failed to parse organization createdAt field"))
	}

	return &central.Organization{
		Id:                        org.ID.Uint64(),
		Name:                      org.Name,
		CreatedAt:                 createdAt,
		Email:                     org.Email,
		Country:                   org.Country,
		Status:                    strings.ToUpper(org.Status),
		Plan:                      strings.ToUpper(org.Plan),
		Timezone:                  org.Timezone,
		TotalCredit:               org.TotalCredit,
		RemainingCredit:           org.RemainingCredit,
		ActiveClustersCount:       org.ActiveClustersCount,
		FreeClustersCount:         org.FreeClustersCount,
		HasDiscount:               org.HasDiscount,
		DiscountDescription:       org.DiscountDescription,
		Discount:                  org.Discount,
		AcknowledgeCnPolicy:       org.AcknowledgeCnPolicy,
		HasTriedDevTier:           org.HasTriedDevTier,
		ThirdPartyAccountProvider: org.ThirdPartyAccountProvider,
		ThirdPartyAccountCode:     org.ThirdPartyAccountCode,
		WhitelistFlag:             org.WhitelistFlag,
	}, nil
}

func (s *tenantService) UpdateOrganization(ctx context.Context, req *central.UpdateOrgReq) (*types.Empty, error) {
	userContext := s.userContext(ctx)
	err := userContext.CheckOrgID(entity.ID(req.OrgId))
	if err != nil {
		return &types.Empty{}, err
	}
	err = userContext.CheckIfOwner()
	if err != nil {
		return nil, err
	}

	request := &central.UpdateOrgReq{
		Name:     s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.Name)),
		Timezone: req.Timezone,
	}
	err = validateUpdateOrgReq(request)
	if err != nil {
		return &types.Empty{}, errcode.NewInvalidInputErr(err)
	}
	err = userContext.UpdateOrganization(ctx, request)
	if err != nil {
		return &types.Empty{}, err
	}
	return &types.Empty{}, nil
}

// TODO(zheming) : should change name to payment created notify
func (s *tenantService) UpdateOrgDefaultPayment(ctx context.Context, req *central.UpdateOrgDefaultPaymentReq) (*types.Empty, error) {
	userContext := s.userContext(ctx)
	err := userContext.CheckOrgID(entity.ID(req.OrgId))
	if err != nil {
		return &types.Empty{}, err
	}
	err = userContext.CheckIfOwnerOrBillingAdmin()
	if err != nil {
		return nil, err
	}
	err = userContext.BillingContext().EnableOrgOnDemandPlan()
	if err != nil {
		return nil, err
	}
	return &types.Empty{}, nil
}

func (s *tenantService) AcknowledgeCNPolicy(ctx context.Context, req *central.AcknowledgeCNPolicyReq) (*types.Empty, error) {
	orgID := entity.ID(req.OrgId)
	userContext := s.userContext(ctx)
	err := userContext.CheckOrgID(orgID)
	if err != nil {
		return nil, err
	}

	ipAddress, err := service.GetClientIP(ctx)
	if err != nil {
		return nil, errcode.NewInternalErr(errors.New("failed to get client IP address"))
	}
	err = userContext.AcknowledgeCNPolicy(ipAddress)
	if err != nil {
		return nil, errcode.NewInternalErr(errors.New("failed to acknowledge CN policy"))
	}

	return &types.Empty{}, nil
}

func (s *tenantService) ListUsers(ctx context.Context, req *central.ListUsersReq) (*central.Users, error) {
	orgID := entity.ID(req.OrgId)
	userContext := s.userContext(ctx)
	err := userContext.CheckOrgID(orgID)
	if err != nil {
		return nil, err
	}

	filter := tenant.ListUsersReq{
		TenantID: orgID,
	}
	if req.Email != "" {
		filter.Email = &req.Email
	}

	userList, err := s.userContext(ctx).ListUsers(filter)
	logger := s.Log(ctx)
	if err != nil {
		return nil, err
	}
	users := usersOutput(logger, userList)

	return &central.Users{
		Total: int32(len(users)),
		Items: service.Pagination(req.Page, req.PerPage, users).([]*central.User),
	}, nil
}

func (s *tenantService) UpdateUser(ctx context.Context, req *central.UpdateUserReq) (*types.Empty, error) {
	orgID := entity.ID(req.OrgId)
	userID := entity.ID(req.UserId)
	role := entity.UserRole(req.Role)
	userContext := s.userContext(ctx)

	err := userContext.CheckOrgID(orgID)
	if err != nil {
		return nil, err
	}
	err = userContext.CheckIfOwner()
	if err != nil {
		return nil, err
	}

	updateUserReq := tenant.UpdateUserReq{
		OrgID:  orgID,
		UserID: userID,
		Role:   role,
	}
	err = validateUpdateUserReq(updateUserReq)
	if err != nil {
		return nil, errcode.NewInvalidInputErr(err)
	}

	return &types.Empty{}, userContext.UpdateUser(ctx, updateUserReq)
}

func usersOutput(logger *zap.Logger, users []*tenant.User) []*central.User {
	var opUsers []*central.User
	for _, user := range users {
		opUser := &central.User{
			Id:     user.ID,
			Email:  user.Email,
			Name:   user.Name,
			Status: strings.ToUpper(user.Status),
			Role:   user.Role,
		}

		if user.InvitedAt != nil {
			inviteTime, err := types.TimestampProto(*user.InvitedAt)
			if err != nil {
				logger.Error("failed to parse user's InviteTime field", zap.Error(err))
			} else {
				opUser.InviteTime = inviteTime
			}
		}

		opUsers = append(opUsers, opUser)
	}
	return opUsers
}

func (s *tenantService) DeleteUser(ctx context.Context, req *central.DeleteUserReq) (*types.Empty, error) {
	orgID := entity.ID(req.OrgId)
	userID := entity.ID(req.UserId)
	userContext := s.userContext(ctx)
	err := userContext.CheckOrgID(orgID)
	if err != nil {
		return nil, err
	}
	err = userContext.CheckIfOwner()
	if err != nil {
		return nil, err
	}

	return &types.Empty{}, userContext.DeleteUser(orgID, userID)
}

func (s *tenantService) CreateAuth0User(ctx context.Context, req *central.CreateAuth0UserReq) (*types.Empty, error) {
	userContext := s.tenantModel.User(ctx, &tenantCentral.User{})
	orgID := entity.ID(req.OrgId)
	password := req.Password
	err := service.IsValidUserPassword(password)
	if err != nil {
		return nil, err
	}
	if err := userContext.CreateAuth0User(ctx, orgID, req.GetToken(), password); err != nil {
		return nil, err
	}
	return &types.Empty{}, nil
}

func (s *tenantService) AcceptInvitation(ctx context.Context, req *central.AcceptInvitationReq) (*types.Empty, error) {
	tenantID := entity.ID(req.OrgId)
	user := s.CurrentUser(ctx)
	email := strings.ToLower(req.GetEmail())
	auth0email := user.Email
	if auth0email != email {
		return nil, errcode.NewInvalidInputErr(errors.Errorf("the email address in the req %s and bearer token %s is inconsistent", email, auth0email))
	}

	userContext := s.tenantModel.User(
		ctx,
		&tenantCentral.User{
			TenantID: tenantID,
			User: entity.User{
				Email: email,
			},
		})

	if err := userContext.AcceptInvitation(tenantID, req.GetEmail(), req.GetFirstName(), req.GetLastName()); err != nil {
		log.Error(ctx, "failed to accept invitation", zap.String("feature", "invitation"), zap.String("email", email), zap.Error(err))
		return nil, err
	}
	log.Info(ctx, "accept invitation", zap.String("feature", "invitation"), zap.String("email", email))
	return &types.Empty{}, nil
}

func (s *tenantService) GetUserProfile(ctx context.Context, req *central.OrgIdReq) (*central.UserProfile, error) {
	userContext := s.userContext(ctx)
	return userContext.GetUpdateUserProfile(ctx, entity.ID(req.OrgId))
}

func (s *tenantService) UpdateUserProfile(ctx context.Context, req *central.UpdateUserProfileReq) (*types.Empty, error) {
	firstName := s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.FirstName))
	lastName := s.SanitizerPolicy().Sanitize(strings.TrimSpace(req.LastName))
	err := service.IsValidUsername(firstName, lastName)
	if err != nil {
		return &types.Empty{}, errcode.NewInvalidInputErr(err)
	}
	user := s.CurrentUser(ctx)
	updateUserProfileReq := tenant.UpdateUserProfileReq{
		Email:     user.Email,
		FirstName: firstName,
		LastName:  lastName,
	}
	err = s.userContext(ctx).UpdateUserProfile(entity.ID(req.OrgId), updateUserProfileReq)
	if err != nil {
		return &types.Empty{}, err
	}
	return &types.Empty{}, nil
}

func (s *tenantService) GetClientIP(ctx context.Context, _ *types.Empty) (*central.GetClientIPResp, error) {
	ip, err := service.GetClientIP(ctx)
	if err != nil {
		return nil, errcode.NewInternalErr(errors.New("failed to get client IP address"))
	}

	return &central.GetClientIPResp{
		Ip: ip,
	}, nil
}

func (s *tenantService) ListTenantAccessLogs(c context.Context, req *central.ListAccessLogReq) (*central.ListAccessLogResp, error) {
	ret, err := service.ListAccessLogs(s.userContext(c).ListAccessLog(&model.ListAccessLogQuery{
		StartTimeInSec: req.StartTime,
		EndTimeInSec:   req.EndTime,
		User:           req.User,
		Content:        req.Content,
		From:           service.GetStartIndex(int(req.Page), int(req.PerPage)),
		Size:           service.GetSize(int(req.PerPage)),
	}))
	if err != nil {
		return ret, errcode.NewInternalErr(errors.New("failed to list tenant access logs"))
	}
	return ret, nil
}
