package org

import (
	"context"
	"fmt"

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

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

	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/tiinf/errs"

	"go.uber.org/zap"

	"github.com/tidbcloud/tiinf/monitor/tilog"

	"github.com/jinzhu/copier"

	"github.com/pingcap/errors"
	"github.com/tidbcloud/account/commons/config"
	"github.com/tidbcloud/account/internel/dao/entity"
	"gorm.io/gorm"
)

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

// SaveTenant saves tenant and related table records to database, the method is used for user registration.
func SaveTenant(ctx context.Context, tx *gorm.DB, tenantGroup *TenantGroup) (*entity.Tenant, error) {
	tenant := tenantGroup.Tenant
	user := tenantGroup.User
	project := tenantGroup.Project
	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
	// if user is exits update
	// not exist create
	dbUser := &entity.User{}
	err = entity.NewUserQuerySet(tx).EmailEq(user.Email).StatusEq(entity.UserStatusPending).One(dbUser)
	tilog.Info(ctx, "query user by email ", zap.Any("user", dbUser))
	if err == gorm.ErrRecordNotFound {
		tilog.Info(ctx, "user not exist, will create user", zap.Any("user", user))
		err = user.Create(tx)
		if err != nil {
			return tenant, errors.Annotate(err, "create users record")
		}
	} else if err != nil {
		return tenant, errors.Annotate(err, "fail get users record")
	} else {

		// the email user is exits, update
		user.ID = dbUser.ID
		_ = copier.Copy(dbUser, user)
		tilog.Info(ctx, "update user ", zap.Any("user", dbUser))
		err = (dbUser).Update(tx,
			entity.UserDBSchema.FirstName,
			entity.UserDBSchema.LastName,
			entity.UserDBSchema.Status,
			entity.UserDBSchema.InvitedAt,
			entity.UserDBSchema.TenantID,
			entity.UserDBSchema.Role,
			entity.UserDBSchema.PhoneNumber,
			entity.UserDBSchema.Job,
		)
		if err != nil {
			return nil, errors.Annotate(err, "update user record")
		}
	}

	// for custom email, need to verify email, user status is pending
	roleBindingStatus := entity.RoleBindingStatusInactive
	if user.Status == entity.UserStatusActive {
		roleBindingStatus = entity.RoleBindingStatusActive
	}
	_, err = rbac.AddOrgRoleForUser(ctx, tx, user.ID, tenant.ID, user.Role.Encode(), roleBindingStatus)
	if err != nil {
		return tenant, errors.Annotate(err, "create role bindings 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
	//	// delete quota when cm is ready (wangletian)
	//	err = (&quota).Create(tx)
	//	if err != nil {
	//		return tenant, errors.Annotate(err, "create quota record")
	//	}
	//}
	project.TenantID = tenant.ID
	project.Users = []entity.User{*user}
	err = project.Create(tx)
	tilog.Info(ctx, "Create project finished.", zap.Any("project", project))
	if err != nil {
		return tenant, err
	}
	// not assign role on tidbx hidden project
	if project.Type != entity.ProjectTypeTiDBXVirtual {
		_, err = rbac.AddProjectRoleForUser(ctx, tx, user.ID, project.ID, entity.RoleProjectOwner, roleBindingStatus)
		if err != nil {
			return tenant, errors.Annotate(err, "create project role for user failed")
		}
	}

	// send account data change event
	go func() {
		_ = comsvc.SendUserRoleBindingChangeEvent(context.Background(), user.ID.Uint64(), tenant.ID.Uint64(), "Registration")
	}()

	return tenant, nil
}

func UpdateTenantStatus(tx *gorm.DB, id entity.ID, status entity.TenantStatus) (err error) {
	var tenant entity.Tenant

	err = entity.NewTenantQuerySet(tx).IDEq(id).One(&tenant)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return errs.NewBadRequest("Tenant not found", nil)
		}
		return err
	}

	tenant.Status = status
	return tenant.Update(tx, entity.TenantDBSchema.Status)
}

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

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

func ListOrgsByIDList(ctx context.Context, tx *gorm.DB, orgIDs []entity.ID) ([]entity.Tenant, error) {
	var orgs []entity.Tenant
	if len(orgIDs) == 0 {
		return orgs, nil
	}
	//err := entity.NewTenantQuerySet(tx.Preload("Quotas")).IDIn(orgIDs...).All(&orgs)
	err := entity.NewTenantQuerySet(tx).IDIn(orgIDs...).All(&orgs)
	return orgs, err
}

func LegacyGetOrgByUser(ctx context.Context, tx *gorm.DB, userID entity.ID) (*entity.Tenant, bool, error) {
	orgs, err := ListOrgsByUserID(ctx, tx, userID)
	if err != nil {
		tilog.Error(ctx, "ListOrgsByUserID return error", zap.Any("userID", userID), zap.Any("err", err))
		return nil, false, err
	}
	if len(orgs) == 0 {
		tilog.Info(ctx, "ListOrgsByUserID return orgs empty", zap.Any("userID", userID))
		return nil, false, nil
	}
	return &orgs[0], true, nil
}

func ListOrgsByUserIDStatus(ctx context.Context, tx *gorm.DB, userID entity.ID, status []string) ([]entity.Tenant, error) {
	var roles []entity.RoleBinding
	db := tx.Where(
		"target_type = ? AND target_id = ? AND scope_type = ?",
		entity.RoleBindingTargetUser,
		userID,
		entity.RoleBindingScopeOrg,
	)
	if len(status) > 0 {
		db = db.Where("status in (?)", status)
	}
	err := db.Find(&roles).Error
	if err != nil {
		return nil, err
	}

	mapUserOrgs := map[entity.ID]bool{}
	listUserOrgs := make([]entity.ID, 0, len(roles))
	for _, role := range roles {
		if _, ok := mapUserOrgs[role.ScopeID]; !ok {
			mapUserOrgs[role.ScopeID] = true
			listUserOrgs = append(listUserOrgs, role.ScopeID)
		}
	}

	return ListOrgsByIDList(ctx, tx, listUserOrgs)
}

func ListOrgsByUserID(ctx context.Context, tx *gorm.DB, userID entity.ID) ([]entity.Tenant, error) {
	return ListOrgsByUserIDStatus(ctx, tx, userID, []string{entity.RoleBindingStatusActive})
}

func IsUserInOrg(ctx context.Context, tx *gorm.DB, userID entity.ID, orgId entity.ID) (bool, error) {
	var roles []entity.RoleBinding
	err := tx.Where(
		"target_type = ? AND target_id = ? AND scope_type = ? AND scope_id = ? AND status = ?",
		entity.RoleBindingTargetUser,
		userID,
		entity.RoleBindingScopeOrg,
		orgId,
		entity.RoleBindingStatusActive,
	).Find(&roles).Error

	if err != nil {
		return false, err
	}

	for _, role := range roles {
		if role.Role != "" {
			return true, nil
		}
	}

	return false, nil
}

func GetAllUnSyncedSalesforceRecords(ctx context.Context, tx *gorm.DB) ([]entity.TenantSalesforceSyncRecord, error) {
	var records []entity.TenantSalesforceSyncRecord

	err := entity.NewTenantSalesforceSyncRecordQuerySet(tx).SyncedEq(false).All(&records)

	return records, err
}
