package admin

import (
	"fmt"

	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/dbutil"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"gorm.io/gorm"
	"k8s.io/utils/pointer"
)

type Region struct {
	Id                uint64
	Provider          string
	Name              string
	DisplayName       string
	Status            string
	Zones             []string
	IsPublic          bool
	OpenedOrgIDs      []uint64
	DefaultBackupTime string
}

func validateOpenedOrgIDs(tx *gorm.DB, region *Region) ([]entity.Tenant, error) {
	var tenants []entity.Tenant
	if !region.IsPublic {
		var ids []entity.ID
		for _, id := range region.OpenedOrgIDs {
			ids = append(ids, entity.ID(id))
		}
		err := entity.NewTenantQuerySet(tx).IDIn(ids...).All(&tenants)
		if err != nil {
			return tenants, err
		}
		if len(tenants) != len(region.OpenedOrgIDs) {
			return tenants, errcode.NewInvalidInputErr(fmt.Errorf("unknown OpenedOrgIDs: %v", region.OpenedOrgIDs))
		}
	}
	return tenants, nil
}

func (m UserContext) AddRegion(region *Region) (*Region, error) {
	if err := validateRegion(region); err != nil {
		return nil, err
	}
	if m.tenantAndUser.ID == 0 {
		return nil, errcode.NewInvalidInputErr(errors.New("not a normal user"))
	}
	var (
		provider entity.Provider
		dbRegion entity.Region
	)
	err := db.Transaction(func(tx *gorm.DB) error {
		err := entity.NewProviderQuerySet(tx).NameEq(entity.Cloud(region.Provider)).One(&provider)
		if err != nil {
			return err
		}

		tenants, err := validateOpenedOrgIDs(tx, region)
		if err != nil {
			return err
		}

		dbRegion = buildRegion(region, provider, tenants)
		if err = tx.Create(&dbRegion).Error; err != nil {
			return err
		}
		desc := fmt.Sprintf("add new region %s for provider %s", region.Name, region.Provider)
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.User.TenantID, m.tenantAndUser.Email, desc)
	})
	if err != nil {
		return nil, err
	}
	return buildInternalRegion(dbRegion, provider), nil
}

func (m UserContext) UpdateRegion(region *Region) (*Region, error) {
	if err := validateRegion(region); err != nil {
		return nil, err
	}
	if m.tenantAndUser.ID == 0 {
		return nil, errcode.NewInvalidInputErr(errors.New("not a normal user"))
	}

	var (
		provider entity.Provider
		dbRegion entity.Region
	)
	err := db.Transaction(func(tx *gorm.DB) error {
		err := entity.NewProviderQuerySet(tx).NameEq(entity.Cloud(region.Provider)).One(&provider)
		if err != nil {
			return err
		}
		if err = entity.NewRegionQuerySet(tx.Preload("Zones").Preload("Tenants")).IDEq(entity.ID(region.Id)).One(&dbRegion); err != nil {
			return errors.Annotate(err, fmt.Sprintf("query region failed, %d", region.Id))
		}

		tenants, err := validateOpenedOrgIDs(tx, region)
		if err != nil {
			return err
		}

		var newList []*entity.Zone
		var dbZoneMap = make(map[string]*entity.Zone)
		for _, item := range dbRegion.Zones {
			dbZoneMap[item.Name] = item
		}

		var keepMap = make(map[string]bool)
		for _, zn := range region.Zones {
			oldZ, found := dbZoneMap[zn]
			if found {
				keepMap[zn] = true
				newList = append(newList, oldZ)
			} else {
				newList = append(newList, &entity.Zone{
					Name:        zn,
					DisplayName: zn,
					RegionID:    0,
				})
			}
		}
		for _, item := range dbRegion.Zones {
			_, ok := keepMap[item.Name]
			if !ok {
				if err = tx.Delete(item).Error; err != nil {
					return errors.Annotate(err, fmt.Sprintf("delete zone failed %s", item.Name))
				}
			}
		}

		dbRegion.Name = region.Name
		dbRegion.DisplayName = region.DisplayName
		dbRegion.Status = entity.RegionStatus(region.Status)
		dbRegion.IsPublic = pointer.BoolPtr(region.IsPublic)
		dbRegion.Tenants = tenants
		dbRegion.Zones = newList
		dbRegion.DefaultBackupTime = region.DefaultBackupTime
		rx := &dbRegion
		if err = rx.Update(tx, entity.RegionDBSchema.DisplayName, entity.RegionDBSchema.Status, entity.RegionDBSchema.IsPublic, entity.RegionDBSchema.DefaultBackupTime); err != nil {
			return errors.Annotate(err, fmt.Sprintf("update region %d", dbRegion.ID))
		}
		err = tx.Model(&dbRegion).Association("Tenants").Replace(tenants)
		if err != nil {
			return errors.Annotate(err, "update region openedOrgIDs")
		}
		desc := fmt.Sprintf("update region %s for provider %s", region.Name, region.Provider)
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.User.TenantID, m.tenantAndUser.Email, desc)
	})
	if err != nil {
		return nil, err
	}
	return buildInternalRegion(dbRegion, provider), nil
}

func validateRegion(region *Region) error {
	if region.DisplayName == "" {
		return errcode.NewInvalidInputErr(errors.Errorf("invalid region display name %s", region.DisplayName))
	}
	if region.Name == "" {
		return errcode.NewInvalidInputErr(errors.Errorf("invalid region name %s", region.Name))
	}
	if len(region.Zones) != 3 {
		return errcode.NewInvalidInputErr(errors.New("invalid zone size, must be 3"))
	}
	if !region.IsPublic && len(region.OpenedOrgIDs) == 0 {
		return errcode.NewInvalidInputErr(errors.New("the region is not public, you need to set OpenedOrgIDs field"))
	}
	if region.IsPublic && len(region.OpenedOrgIDs) > 0 {
		return errcode.NewInvalidInputErr(errors.New("the region is public, you don't need to set OpenedOrgIDs field"))
	}
	return nil
}

func buildRegion(region *Region, provider entity.Provider, tenants []entity.Tenant) entity.Region {
	dbRegion := entity.Region{
		Name:              region.Name,
		DisplayName:       region.DisplayName,
		Status:            entity.RegionStatus(region.Status),
		ProviderID:        provider.ID,
		IsPublic:          pointer.BoolPtr(region.IsPublic),
		Tenants:           tenants,
		DefaultBackupTime: region.DefaultBackupTime,
	}
	dbRegion.ID = entity.ID(region.Id)
	for _, zone := range region.Zones {
		dbRegion.Zones = append(dbRegion.Zones, &entity.Zone{Name: zone})
	}
	return dbRegion
}

func buildInternalRegion(item entity.Region, provider entity.Provider) *Region {
	var zones []string
	for _, item := range item.Zones {
		zones = append(zones, item.Name)
	}

	var openedOrgIDs []uint64
	var isPublic bool
	if item.IsPublic != nil {
		isPublic = *item.IsPublic
	}
	if !isPublic {
		for _, tenant := range item.Tenants {
			openedOrgIDs = append(openedOrgIDs, tenant.ID.Uint64())
		}
	}

	return &Region{
		Id:                uint64(item.ID),
		Provider:          provider.Name.String(),
		Zones:             zones,
		Name:              item.Name,
		Status:            string(item.Status),
		DisplayName:       item.DisplayName,
		OpenedOrgIDs:      openedOrgIDs,
		DefaultBackupTime: item.DefaultBackupTime,
		IsPublic:          isPublic,
	}
}

func (m UserContext) DeleteRegion(providerId string, id uint64) error {
	if m.tenantAndUser.ID == 0 {
		return errors.New("not a normal user")
	}

	return db.Transaction(func(tx *gorm.DB) error {
		provider, err := dbutil.GetProvider(tx, entity.Cloud(providerId))
		if err != nil {
			return err
		}

		var region entity.Region
		err = entity.NewRegionQuerySet(tx.Preload("Zones")).IDEq(entity.ID(id)).One(&region)
		if err != nil {
			return err
		}
		if region.ProviderID != provider.ID {
			return errors.New("invalid provider")
		}
		lck.Lock()
		defer lck.Unlock()
		//profiles, err := db.GetClusterProfilesByRegions(tenant.GetCentralDB(), []entity.ID{entity.ID(id)})
		//if err != nil {
		//	return err
		//}
		//if len(profiles) > 0 {
		//	return errors.New("this region is not empty, please delete all profiles first")
		//}

		// Delete zones related to the deleted region.
		err = entity.NewZoneQuerySet(tx).RegionIDEq(entity.ID(id)).Delete()
		if err != nil {
			return err
		}
		err = entity.NewRegionQuerySet(tx).IDEq(entity.ID(id)).Delete()
		if err != nil {
			return err
		}
		desc := fmt.Sprintf("delete region %s for provider %s", region.Name, provider.Name)
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.User.TenantID, m.tenantAndUser.Email, desc)
	})
}

func (m UserContext) GetRegionByID(providerId string, id uint64) (*Region, error) {
	if m.tenantAndUser.ID == 0 {
		return nil, errors.New("not a normal user")
	}

	var provider entity.Provider
	var region entity.Region
	err := db.Transaction(func(tx *gorm.DB) error {
		err := entity.NewProviderQuerySet(tx).NameEq(entity.Cloud(providerId)).One(&provider)
		if err != nil {
			return err
		}
		return entity.NewRegionQuerySet(tx.Preload("Zones").Preload("Tenants")).IDEq(entity.ID(id)).One(&region)
	})
	if err != nil {
		return nil, err
	}

	if region.ProviderID != provider.ID {
		return nil, errors.New("invalid provider")
	}
	return buildInternalRegion(region, provider), nil
}

func (m UserContext) ListRegions(providerStr string) ([]*Region, error) {
	if m.tenantAndUser.ID == 0 {
		return nil, errors.New("not a normal user")
	}

	var (
		provider  entity.Provider
		dbRegions []entity.Region
	)
	err := db.Transaction(func(tx *gorm.DB) error {
		err := entity.NewProviderQuerySet(tx).NameEq(entity.Cloud(providerStr)).One(&provider)
		if err != nil {
			return err
		}
		return entity.NewRegionQuerySet(tx.Preload("Zones").Preload("Tenants")).ProviderIDEq(provider.ID).All(&dbRegions)
	})
	if err != nil {
		return nil, err
	}

	var regions []*Region
	for _, item := range dbRegions {
		regions = append(regions, buildInternalRegion(item, provider))
	}
	return regions, nil
}

func (m UserContext) GetListRegionsCount(providerStr string) (int64, error) {
	var count int64

	err := db.Transaction(func(tx *gorm.DB) error {
		var provider entity.Provider
		err := entity.NewProviderQuerySet(tx).NameEq(entity.Cloud(providerStr)).One(&provider)
		if err != nil {
			return err
		}
		count, err = entity.NewRegionQuerySet(tx.Preload("Zones")).ProviderIDEq(provider.ID).Count()
		return err
	})

	return count, err
}
