package dbutil

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/pkgv2/log"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type TenantGroup struct {
	Tenant          *entity.Tenant
	User            *entity.User
	RegistrationLog *entity.RegistrationLog
}

// SaveTenant saves tenant and related table records to database, the method is used for user registration.
func SaveTenant(tx *gorm.DB, tenantGroup *TenantGroup) (*entity.Tenant, error) {
	tenant := tenantGroup.Tenant
	user := tenantGroup.User
	registrationLog := tenantGroup.RegistrationLog

	quotas := []entity.Quota{
		{
			ComponentName: entity.TiDB,
			Quota:         config.DefaultTidbQuota,
		},
		{
			ComponentName: entity.TiKV,
			Quota:         config.DefaultTikvQuota,
		},
		{
			ComponentName: entity.TiFlash,
			Quota:         config.DefaultTiflashQuota,
		},
	}

	err := tenant.Create(tx)
	if err != nil {
		return tenant, errors.Annotate(err, "create tenants record")
	}
	user.TenantID = tenant.ID
	err = user.Create(tx)
	if err != nil {
		return tenant, errors.Annotate(err, "create users record")
	}

	userID := user.ID
	registrationLog.UserID = userID
	if registrationLog.InvitedByID == 0 {
		registrationLog.InvitedByID = userID
	}
	err = registrationLog.Create(tx)
	if err != nil {
		return tenant, errors.Annotate(err, "create registration_logs record")
	}
	for _, quota := range quotas {
		quota.TenantID = tenant.ID
		err = (&quota).Create(tx)
		if err != nil {
			return tenant, errors.Annotate(err, "create quota record")
		}
	}
	project := &entity.Project{
		TenantID: tenant.ID,
		Name:     config.DefaultProjectName,
		Users:    []entity.User{*user},
		Status:   entity.ProjectStatusActive,
	}
	err = project.Create(tx)
	if err != nil {
		return tenant, err
	}

	return tenant, nil
}

func UpsertUserInviteToken(tx *gorm.DB, userInviteToken entity.UserInviteToken) (entity.UserInviteToken, error) {
	var existingUserInviteToken entity.UserInviteToken
	err := entity.NewUserInviteTokenQuerySet(tx).MethodEq(
		userInviteToken.Method).EmailEq(userInviteToken.Email).One(&existingUserInviteToken)
	if err == nil {
		userInviteToken.ID = existingUserInviteToken.ID
		err = userInviteToken.Update(
			tx,
			entity.UserInviteTokenDBSchema.TenantID,
			entity.UserInviteTokenDBSchema.BeginAt,
			entity.UserInviteTokenDBSchema.ExpireAt,
			entity.UserInviteTokenDBSchema.IsOneTime,
		)
	} else if nil != errors.Find(err, func(e error) bool { return e == gorm.ErrRecordNotFound }) {
		err = userInviteToken.Create(tx)
	}
	return userInviteToken, errors.Annotate(err, "Upsert User Invite Token")
}

func UpdateRegistration(tx *gorm.DB, email string) error {
	tenantEntity := entity.Tenant{}
	err := entity.NewTenantQuerySet(tx).EmailEq(email).One(&tenantEntity)
	if err != nil {
		return errors.Annotate(TagNotFound(err), "Update Registration")
	}

	userEntity := entity.User{}
	err = entity.NewUserQuerySet(tx).EmailEq(email).DeletedAtIsNull().One(&userEntity)
	if err != nil {
		return errors.Annotate(TagNotFound(err), "Update Registration")
	}

	tenantEntity.Status = entity.TenantStatusActive
	userEntity.Status = entity.UserStatusActive
	err = tenantEntity.Update(tx, entity.TenantDBSchema.Status)
	if err != nil {
		return errors.Annotate(err, "Update Registration")
	}
	err = userEntity.Update(tx, entity.UserDBSchema.Status)
	if err != nil {
		return errors.Annotate(err, "Update Registration")
	}
	return nil
}

func GetTenantByID(tx *gorm.DB, tenantID entity.ID) (entity.Tenant, error) {
	tenant := entity.Tenant{}
	err := entity.NewTenantQuerySet(tx.Preload("Quotas")).IDEq(tenantID).One(&tenant)
	return tenant, TagNotFound(err)
}

type UserFilter struct {
	TenantID *entity.ID
	Email    *string
	Statuses []entity.UserStatus
	Roles    []entity.UserRole
}

// ListUsers returns a list of organization users, default OrderDescByCreatedAt.
func ListUsers(tx *gorm.DB, filter UserFilter) ([]entity.User, error) {
	var users []entity.User
	qs := entity.NewUserQuerySet(tx)
	if filter.TenantID != nil {
		qs = qs.TenantIDEq(*filter.TenantID)
	}
	if filter.Email != nil {
		qs = qs.EmailLike(fmt.Sprintf("%%%s%%", *filter.Email))
	}
	if len(filter.Statuses) > 0 {
		qs = qs.StatusIn(filter.Statuses...)
	}
	if len(filter.Roles) > 0 {
		qs = qs.RoleIn(filter.Roles...)
	}

	err := qs.OrderDescByCreatedAt().All(&users)
	if err != nil {
		return nil, errors.AddStack(err)
	}
	return users, nil
}

func GetUserByID(tx *gorm.DB, orgID, userID entity.ID) (entity.User, error) {
	user := entity.User{}
	err := entity.NewUserQuerySet(tx.Preload("Projects")).TenantIDEq(orgID).IDEq(userID).One(&user)
	return user, TagNotFound(err)
}

func GetUserByEmail(tx *gorm.DB, email string) (entity.User, error) {
	user := entity.User{}
	err := entity.NewUserQuerySet(tx.Preload("Projects")).EmailEq(email).PreloadTenant().One(&user)
	return user, TagNotFound(err)
}

type CreateUserRequired struct {
	Email     string
	FirstName string
	LastName  string
	InviteAt  *time.Time
	Status    entity.UserStatus
	Role      entity.UserRole
	TenantID  entity.ID
	Projects  []entity.Project
}

func UpsertUser(tx *gorm.DB, input CreateUserRequired) error {
	user := entity.User{
		Email:     input.Email,
		FirstName: input.FirstName,
		LastName:  input.LastName,
		Status:    input.Status,
		InvitedAt: input.InviteAt,
		TenantID:  input.TenantID,
		Role:      input.Role,
	}

	var existingUser entity.User
	err := entity.NewUserQuerySet(tx).EmailEq(user.Email).DeletedAtIsNull().One(&existingUser)
	if err != nil && err != gorm.ErrRecordNotFound {
		return err
	}

	if err == gorm.ErrRecordNotFound {
		err = (&user).Create(tx)
		if err != nil {
			return errors.Annotate(err, "create user record")
		}
	} else {
		user.ID = existingUser.ID
	}

	err = tx.Model(&user).Association("Projects").Replace(input.Projects)
	if err != nil {
		return errors.Annotate(err, "update user projects")
	}
	return (&user).Update(tx,
		entity.UserDBSchema.FirstName,
		entity.UserDBSchema.LastName,
		entity.UserDBSchema.Status,
		entity.UserDBSchema.InvitedAt,
		entity.UserDBSchema.TenantID,
		entity.UserDBSchema.Role,
	)
}

func TenantSystemUserEmail(tenantID entity.ID) string {
	return fmt.Sprintf("%d-%s", tenantID, config.SystemEmail)
}

// SystemUser gets or creates a system user
// TODO(Louis): add log context parameter
func SystemUser(tx *gorm.DB, tenantID entity.ID) (entity.User, error) {
	su, err := GetUserByEmail(tx, TenantSystemUserEmail(tenantID))
	if err == nil {
		return su, nil
	} else if nil == errors.Find(err, func(e error) bool { return e == gorm.ErrRecordNotFound }) {
		return su, err
	}
	log.Info(context.TODO(), "seeding a system user")

	user := entity.User{
		Email:     TenantSystemUserEmail(tenantID),
		FirstName: "system",
		LastName:  "user",
		Status:    entity.UserStatusActive,
		TenantID:  tenantID,
	}

	err = user.Create(tx)
	if err != nil {
		return user, errors.Annotate(err, "error while seeding system user")
	}

	log.Info(context.TODO(), "system user created", zap.Uint64("userID", uint64(user.ID)))
	return user, nil
}

func DeleteProject(ctx context.Context, tx *gorm.DB, orgID, projectID entity.ID) error {
	log.WithContextL(ctx).Info("delete project", zap.Uint64("projectID", projectID.Uint64()))
	now := time.Now()
	err := entity.NewProjectQuerySet(tx).TenantIDEq(orgID).IDEq(projectID).StatusEq(entity.ProjectStatusActive).GetUpdater().
		SetDeletedTime(&now).SetStatus(entity.ProjectStatusDeleted).Update()
	if err != nil {
		return errors.Annotate(err, "failed to update project status")
	}
	return nil
}

func ClearProject(ctx context.Context, tx *gorm.DB, projectID entity.ID) error {
	log.WithContextL(ctx).Info("clear project", zap.Uint64("projectID", projectID.Uint64()))
	err := entity.NewProjectQuerySet(tx).IDEq(projectID).StatusEq(entity.ProjectStatusDeleted).GetUpdater().
		SetStatus(entity.ProjectStatusCleared).Update()
	if err != nil {
		return errors.Annotatef(err, "failed to update status of project %d to cleared", projectID)
	}
	return nil
}

func GetActiveProjects(ctx context.Context, tx *gorm.DB, orgID entity.ID) ([]entity.Project, error) {
	var projects []entity.Project
	err := entity.NewProjectQuerySet(tx).TenantIDEq(orgID).StatusEq(entity.ProjectStatusActive).All(&projects)
	if err != nil {
		log.WithContextL(ctx).Error("failed to query active projects", zap.Uint64("tenantID", orgID.Uint64()))
		return nil, errors.Annotatef(err, "failed to query active projects of org %d", orgID.Uint64())
	}
	return projects, nil
}

func GetActiveOwnerCount(tx *gorm.DB, orgID entity.ID) (int64, error) {
	return entity.NewUserQuerySet(tx).TenantIDEq(orgID).RoleEq(entity.UserRoleOwner).StatusEq(entity.UserStatusActive).Count()
}

type ProjectUserFilter struct {
	ProjectID entity.ID
	Email     *string
	Statuses  []entity.UserStatus
}

func UserStatusContains(statuses []entity.UserStatus, status entity.UserStatus) bool {
	for _, s := range statuses {
		if s == status {
			return true
		}
	}
	return false
}

// ListProjectUsers returns a list of project users
func ListProjectUsers(tx *gorm.DB, filter ProjectUserFilter) ([]entity.User, error) {
	var project entity.Project
	err := entity.NewProjectQuerySet(tx.Preload("Users")).IDEq(filter.ProjectID).StatusEq(entity.ProjectStatusActive).One(&project)
	if err != nil {
		return nil, errors.Annotatef(err, "failed to get project %d", filter.ProjectID.Uint64())
	}

	var users []entity.User
	// TODO(Louis): filter by db query
	for _, user := range project.Users {
		if filter.Email != nil && !strings.Contains(user.Email, *filter.Email) {
			continue
		}
		if len(filter.Statuses) > 0 && !UserStatusContains(filter.Statuses, user.Status) {
			continue
		}
		users = append(users, user)
	}

	return users, nil
}
