package dbutil

import (
	"fmt"
	"time"

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

func RegisterTenant(tx *gorm.DB, tenant *entity.Tenant, user *entity.User, registrationLog *entity.RegistrationLog) (*entity.Tenant, error) {
	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")
	}

	registrationLog.UserID = user.ID
	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},
	}
	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) (*entity.Tenant, *entity.User, error) {
	tenantEntity := entity.Tenant{}
	err := entity.NewTenantQuerySet(tx).EmailEq(email).One(&tenantEntity)
	if err != nil {
		return nil, nil, errors.Annotate(TagNotFound(err), "Update Registration")
	}

	userEntity := entity.User{}
	err = entity.NewUserQuerySet(tx).EmailEq(email).DeletedAtIsNull().One(&userEntity)
	if err != nil {
		return nil, nil, 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 nil, nil, errors.Annotate(err, "Update Registration")
	}
	err = userEntity.Update(tx, entity.UserDBSchema.Status)
	if err != nil {
		return nil, nil, errors.Annotate(err, "Update Registration")
	}
	return &tenantEntity, &userEntity, 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)
}

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 GetComponentQuota(t *entity.Tenant, name entity.ComponentName) *entity.Quota {
	for _, q := range t.Quotas {
		if q.ComponentName == name {
			return q
		}
	}

	return nil
}

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
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("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("system user created", zap.Uint64("userID", uint64(user.ID)))
	return user, nil
}
