package admin

import (
	"fmt"

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/central-sdk/config"
	"github.com/tidbcloud/central-sdk/db"
	"github.com/tidbcloud/central-sdk/db/dbutil"
	"github.com/tidbcloud/central-sdk/db/entity"
	"gorm.io/gorm"
)

type Region struct {
	Id          uint64
	Provider    string
	Name        string
	DisplayName string
	Status      string
	Zones       []string
}

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(config.Cloud(region.Provider)).One(&provider)
		if err != nil {
			return err
		}
		dbRegion = buildRegion(region, provider)
		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(config.Cloud(region.Provider)).One(&provider)
		if err != nil {
			return err
		}
		if err = entity.NewRegionQuerySet(tx.Preload("Zones")).IDEq(entity.ID(region.Id)).One(&dbRegion); err != nil {
			return errors.Annotate(err, fmt.Sprintf("query region failed, %d", region.Id))
		}

		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 {
				count, err := entity.NewMachineQuerySet(tx).ZoneIDEq(item.ID).Count()
				if err != nil {
					return errors.Annotate(err, fmt.Sprintf("query zone machine failed %s", item.Name))
				}
				if count > 0 {
					return fmt.Errorf("cant not delete zone %s, there are %d machines in that zone", item.Name, count)
				}
				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.Zones = newList
		rx := &dbRegion
		if err = rx.Update(tx, entity.RegionDBSchema.DisplayName, entity.RegionDBSchema.Status); err != nil {
			return errors.Annotate(err, fmt.Sprintf("update region %d", dbRegion.ID))
		}
		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"))
	}
	return nil
}

func buildRegion(region *Region, provider entity.Provider) entity.Region {
	dbRegion := entity.Region{
		Name:        region.Name,
		DisplayName: region.DisplayName,
		Status:      entity.RegionStatus(region.Status),
		ProviderID:  provider.ID,
	}
	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)
	}
	return &Region{
		Id:          uint64(item.ID),
		Provider:    provider.Name.String(),
		Zones:       zones,
		Name:        item.Name,
		Status:      string(item.Status),
		DisplayName: item.DisplayName,
	}
}

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, config.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(config.Cloud(providerId)).One(&provider)
		if err != nil {
			return err
		}
		return entity.NewRegionQuerySet(tx.Preload("Zones")).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(config.Cloud(providerStr)).One(&provider)
		if err != nil {
			return err
		}
		return entity.NewRegionQuerySet(tx.Preload("Zones")).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(config.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
}
