package admin

import (
	"fmt"
	"time"

	"github.com/pingcap/dbaas/pkg/capacity"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"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"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type ProviderClusterProfile struct {
	Id          uint64
	Name        string
	Description string
	Region      string
	Provider    string
	Status      string
	Components  []*ProviderComponentProfile
}

type ProviderComponentProfile struct {
	Name         string
	Replicas     uint32
	MachineType  string
	Price        float64
	DiskCapacity uint32
	DiskType     string
	Memory       uint32
	CPU          uint32
}

var (
	mustHaveComponent = []entity.ComponentName{entity.PD, entity.TiDB, entity.TiKV}
)

func (m UserContext) AddClusterProfile(profile *ProviderClusterProfile) (*ProviderClusterProfile, error) {
	if m.tenantAndUser.ID == 0 {
		return nil, errors.New("not a normal user")
	}
	if err := validateProfile(profile); err != nil {
		return nil, err
	}

	err := db.Transaction(func(tx *gorm.DB) error {
		provider, err := dbutil.GetProvider(tx, config.Cloud(profile.Provider))
		if err != nil {
			return err
		}
		region, err := dbutil.GetRegion(tx, provider.Name, profile.Region)
		if err != nil {
			return err
		}
		dbProfile := &entity.ClusterProfile{
			Name:        profile.Name,
			Description: profile.Description,
			Status:      profile.Status,
			RegionID:    region.ID,
		}
		if err = dbProfile.Create(tx); err != nil {
			return err
		}

		for _, item := range profile.Components {
			//create machine profile is not found
			machineProfile, err := getOrCreateMachineProfile(tx, provider, item)
			if err != nil {
				return err
			}
			//create region machine if not found
			regionMachine, err := getOrCreateRegionMachine(tx, machineProfile, region)
			if err != nil {
				return err
			}
			//create disk profile
			diskProfile, err := getOrCreateDiskProfile(tx, item.DiskType)
			if err != nil {
				return err
			}
			//create region disk
			regionDisk, err := getOrCreateRegionDisk(tx, region, diskProfile, item)
			if err != nil {
				return err
			}

			if item.Name != string(entity.PD) {
				//add region machine price
				if err := tryUpdateRegionDiskPrice(tx, regionDisk); err != nil {
					return err
				}
				//add region machine price
				if err := tryUpdateRegionMachinePrice(tx, regionMachine, item); err != nil {
					return err
				}
			}
			component := &entity.ComponentProfile{
				ComponentName:    entity.ComponentName(item.Name),
				Replicas:         item.Replicas,
				ClusterProfileID: dbProfile.ID,
				MachineProfileID: machineProfile.ID,
				RegionDiskID:     regionDisk.ID,
			}
			// add component
			if err := component.Create(tx); err != nil {
				return err
			}
		}
		profile.Id = dbProfile.ID.Uint64()
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.TenantID, m.tenantAndUser.Email, fmt.Sprintf("add new cluster profile %s for provider %s", profile.Name, profile.Provider))
	})
	if err != nil {
		return nil, err
	}
	return m.GetClusterProfileByID(profile.Provider, profile.Id)
}

func (m UserContext) UpdateClusterProfile(profile *ProviderClusterProfile) (*ProviderClusterProfile, error) {
	if m.tenantAndUser.ID == 0 {
		return nil, errors.New("not a normal user")
	}

	err := db.Transaction(func(tx *gorm.DB) error {
		provider, err := dbutil.GetProvider(tx, config.Cloud(profile.Provider))
		if err != nil {
			return err
		}
		region, err := dbutil.GetRegion(tx, provider.Name, profile.Region)
		if err != nil {
			return err
		}

		var dbProfile entity.ClusterProfile
		if err := entity.NewClusterProfileQuerySet(tx).PreloadRegion().IDEq(entity.ID(profile.Id)).One(&dbProfile); err != nil {
			return errors.Annotate(err, "query cluster profile")
		}
		dbProfile.Status = profile.Status
		dbProfile.Description = profile.Description
		if profile.Name != "" {
			dbProfile.Name = profile.Name
		}
		if err := dbProfile.Update(tx, entity.ClusterProfileDBSchema.Description, entity.ClusterProfileDBSchema.Status, entity.ClusterProfileDBSchema.Name); err != nil {
			return errors.Annotate(err, "update profile")
		}
		var dbComponents []entity.ComponentProfile
		if err := entity.NewComponentProfileQuerySet(tx).ClusterProfileIDEq(dbProfile.ID).All(&dbComponents); err != nil {
			return errors.Annotate(err, fmt.Sprintf("query component profile: %d", dbProfile.ID))
		}
		var cpMap = make(map[string]entity.ComponentProfile)
		for _, cp := range dbComponents {
			cpMap[string(cp.ComponentName)] = cp
		}
		for _, item := range profile.Components {
			//create machine profile is not found
			machineProfile, err := getOrCreateMachineProfile(tx, provider, item)
			if err != nil {
				return err
			}
			//create region machine if not found
			regionMachine, err := getOrCreateRegionMachine(tx, machineProfile, region)
			if err != nil {
				return err
			}
			//create disk profile
			diskProfile, err := getOrCreateDiskProfile(tx, item.DiskType)
			if err != nil {
				return err
			}
			//create region disk
			regionDisk, err := getOrCreateRegionDisk(tx, region, diskProfile, item)
			if err != nil {
				return err
			}

			if item.Name != string(entity.PD) {
				//add region machine price
				if err := tryUpdateRegionDiskPrice(tx, regionDisk); err != nil {
					return err
				}
				//add region machine price
				if err := tryUpdateRegionMachinePrice(tx, regionMachine, item); err != nil {
					return err
				}
			}
			dbCp, found := cpMap[item.Name]
			if found {
				dbCp.RegionDiskID = regionDisk.ID
				dbCp.Replicas = item.Replicas
				dbCp.MachineProfileID = machineProfile.ID
				if err := dbCp.Update(tx, entity.ComponentProfileDBSchema.RegionDiskID, entity.ComponentProfileDBSchema.Replicas, entity.ComponentProfileDBSchema.MachineProfileID); err != nil {
					return err
				}
			} else {
				dbCp = entity.ComponentProfile{
					ComponentName:    entity.ComponentName(item.Name),
					Replicas:         item.Replicas,
					ClusterProfileID: dbProfile.ID,
					MachineProfileID: machineProfile.ID,
					RegionDiskID:     regionDisk.ID,
				}
				if err := tx.Create(&dbCp).Error; err != nil {
					return err
				}
			}
		}
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.TenantID, m.tenantAndUser.Email, fmt.Sprintf("update cluster profile %s for provider %s", profile.Name, profile.Provider))
	})
	if err != nil {
		return nil, err
	}
	return m.GetClusterProfileByID(profile.Provider, profile.Id)
}

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

	err := db.Transaction(func(tx *gorm.DB) error {
		provider, err := dbutil.GetProvider(tx, config.Cloud(providerStr))
		if err != nil {
			return err
		}
		var (
			dbProfile entity.ClusterProfile
		)
		if err := entity.NewClusterProfileQuerySet(tx).PreloadRegion().IDEq(entity.ID(id)).One(&dbProfile); err != nil {
			return errors.Annotate(err, "query cluster profile")
		}

		if err := tx.Where("cluster_profile_id = ?", dbProfile.ID).Delete(&entity.ComponentProfile{}).Error; err != nil {
			log.L().Error("failed to delete component profile", zap.Any("cluster_profile_id", dbProfile.ID), zap.Error(err))
		}

		if err := dbProfile.Delete(tx); err != nil {
			return errors.Annotate(err, "delete cluster profile")
		}
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.TenantID, m.tenantAndUser.Email, fmt.Sprintf("delete cluster profile %s for provider %s", dbProfile.Name, provider.Name))
	})
	if err != nil {
		return err
	}
	return nil
}

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

	var ret *ProviderClusterProfile

	err := db.Transaction(func(tx *gorm.DB) error {
		provider, err := dbutil.GetProvider(tx, config.Cloud(providerStr))
		if err != nil {
			return err
		}
		var regions []entity.Region
		if err := entity.NewRegionQuerySet(tx).ProviderIDEq(provider.ID).All(&regions); err != nil {
			return errors.Annotate(err, "query region")
		}
		var dbProfile entity.ClusterProfile
		if err := entity.NewClusterProfileQuerySet(tx).PreloadRegion().IDEq(entity.ID(id)).One(&dbProfile); err != nil {
			return errors.Annotate(err, "query cluster profile")
		}
		region := dbProfile.Region
		var componentProfiles []entity.ComponentProfile
		if err := entity.NewComponentProfileQuerySet(tx.Preload("RegionDisk.DiskProfile")).PreloadMachineProfile().PreloadRegionDisk().ClusterProfileIDEq(dbProfile.ID).All(&componentProfiles); err != nil {
			return errors.Annotate(err, "query component profile")
		}

		pricesMap, err := buildMachinePricesMap(tx)
		if err != nil {
			return err
		}
		ret = buildInternalClusterProfile(dbProfile, componentProfiles, region, &provider, pricesMap)
		return nil
	})
	return ret, err
}

func (m UserContext) ListProviderClusterProfiles(providerStr string) ([]*ProviderClusterProfile, error) {
	if m.tenantAndUser.ID == 0 {
		return nil, errors.New("not a normal user")
	}
	var internalProfiles []*ProviderClusterProfile
	err := db.Transaction(func(tx *gorm.DB) error {
		provider, err := dbutil.GetProvider(tx, config.Cloud(providerStr))
		if err != nil {
			return err
		}
		var regions []entity.Region
		if err := entity.NewRegionQuerySet(tx).ProviderIDEq(provider.ID).All(&regions); err != nil {
			return errors.Annotate(err, "query region")
		}
		var regionMap = make(map[entity.ID]entity.Region)
		var regionIDS = make([]entity.ID, len(regions))
		for _, item := range regions {
			regionMap[item.ID] = item
			regionIDS = append(regionIDS, item.ID)
		}
		var profiles []entity.ClusterProfile
		if err := entity.NewClusterProfileQuerySet(tx).RegionIDIn(regionIDS...).All(&profiles); err != nil {
			return errors.Annotate(err, "query profile")
		}
		var profileMap = make(map[uint64]entity.ClusterProfile)
		var profileIDS = make([]entity.ID, len(profiles))
		for _, item := range profiles {
			profileMap[item.ID.Uint64()] = item
			profileIDS = append(profileIDS, item.ID)
		}
		var componentProfiles []entity.ComponentProfile
		if err := entity.NewComponentProfileQuerySet(tx.Preload("RegionDisk.DiskProfile")).PreloadMachineProfile().PreloadRegionDisk().ClusterProfileIDIn(profileIDS...).All(&componentProfiles); err != nil {
			return errors.Annotate(err, "query component profile")
		}
		var machineProfileIDs = make([]entity.ID, len(componentProfiles))
		var profileComponentMap = make(map[entity.ID][]entity.ComponentProfile)
		for idx := range componentProfiles {
			item := componentProfiles[idx]
			machineProfileIDs = append(machineProfileIDs, item.MachineProfileID)
			list, found := profileComponentMap[item.ClusterProfileID]
			if !found {
				list = []entity.ComponentProfile{}
			}
			list = append(list, item)
			profileComponentMap[item.ClusterProfileID] = list
		}

		var machinePrices []entity.RegionMachinePrice
		if err := entity.NewRegionMachinePriceQuerySet(tx).PreloadRegionMachine().All(&machinePrices); err != nil {
			return errors.Annotate(err, fmt.Sprintf("failed to get prices of component profiles %v", machineProfileIDs))
		}

		pricesMap, err := buildMachinePricesMap(tx)
		if err != nil {
			return err
		}

		for _, item := range profiles {
			region := regionMap[item.RegionID]
			internalProfiles = append(internalProfiles, buildInternalClusterProfile(item, profileComponentMap[item.ID], &region, &provider, pricesMap))
		}
		return nil
	})

	return internalProfiles, err
}

func validateProfile(profile *ProviderClusterProfile) error {
	if profile.Status != "active" && profile.Status != "inactive" {
		return errcode.NewInvalidInputErr(errors.Errorf("invalid status %s", profile.Status))
	}
	componentMap := map[entity.ComponentName]bool{}
	for _, item := range profile.Components {
		if item.Replicas < 1 {
			return errcode.NewInvalidInputErr(errors.Errorf("%s replicas is invalid", item.Name))
		}
		if item.Price <= 0 && item.Name != string(entity.PD) {
			return errcode.NewInvalidInputErr(errors.Errorf("%s price is invalid", item.Name))
		}
		componentMap[entity.ComponentName(item.Name)] = true
	}
	for _, item := range mustHaveComponent {
		if !componentMap[item] {
			return errcode.NewInvalidInputErr(errors.Errorf("missing component %s", item))
		}
	}
	return nil
}

func buildInternalClusterProfile(profile entity.ClusterProfile, componentProfiles []entity.ComponentProfile, region *entity.Region, provider *entity.Provider, priceMap map[string]entity.RegionMachinePrice) *ProviderClusterProfile {
	var components []*ProviderComponentProfile
	for _, item := range componentProfiles {
		diskType := ""
		var diskCapacity uint32
		if item.RegionDisk != nil {
			diskCapacity = item.RegionDisk.Capacity
			if item.RegionDisk.DiskProfile != nil {
				diskType = string(item.RegionDisk.DiskProfile.Type)
			}
		}
		components = append(components, &ProviderComponentProfile{
			Name:         string(item.ComponentName),
			Replicas:     item.Replicas,
			MachineType:  item.MachineProfile.InstanceType.String(),
			Price:        priceMap[regionMachinePriceKey(profile.RegionID, item.MachineProfileID)].Price,
			Memory:       item.MachineProfile.Memory,
			CPU:          item.MachineProfile.CPU,
			DiskType:     diskType,
			DiskCapacity: diskCapacity,
		})
	}
	return &ProviderClusterProfile{
		Id:          profile.ID.Uint64(),
		Name:        profile.Name,
		Description: profile.Description,
		Region:      region.Name,
		Provider:    string(provider.Name),
		Status:      profile.Status,
		Components:  components,
	}
}

func buildMachinePricesMap(tx *gorm.DB) (map[string]entity.RegionMachinePrice, error) {
	var machinePrices []entity.RegionMachinePrice
	if err := entity.NewRegionMachinePriceQuerySet(tx).PreloadRegionMachine().All(&machinePrices); err != nil {
		return nil, errors.Annotate(err, "failed to get prices of component profiles")
	}

	pricesMap := make(map[string]entity.RegionMachinePrice)
	for _, machinePrice := range machinePrices {
		key := fmt.Sprintf("%d-%d", machinePrice.RegionMachine.RegionID, machinePrice.RegionMachine.MachineProfileID)
		price, exist := pricesMap[key]
		if !exist {
			pricesMap[key] = machinePrice
		} else {
			if machinePrice.CreatedAt.Unix() > price.CreatedAt.Unix() {
				pricesMap[key] = machinePrice
			}
		}
	}
	return pricesMap, nil
}

func regionMachinePriceKey(regionID, machineProfileID entity.ID) string {
	return fmt.Sprintf("%d-%d", regionID, machineProfileID)
}

func tryUpdateRegionDiskPrice(tx *gorm.DB, regionDisk *entity.RegionDisk) error {
	var regionDiskPrice entity.RegionDiskPrice
	err := entity.NewRegionDiskPriceQuerySet(tx).RegionDiskIDEq(regionDisk.ID).OrderDescByEffectedAt().One(&regionDiskPrice)
	if err != nil && err != gorm.ErrRecordNotFound {
		return err
	}
	if err == gorm.ErrRecordNotFound {
		regionDiskPrice = entity.RegionDiskPrice{
			EffectedAt:   time.Now(),
			Price:        0,
			RegionDiskID: regionDisk.ID,
		}
		if err = tx.Create(&regionDiskPrice).Error; err != nil {
			return err
		}
	}
	return nil
}

func getOrCreateRegionDisk(tx *gorm.DB, region *entity.Region, diskProfile *entity.DiskProfile, item *ProviderComponentProfile) (*entity.RegionDisk, error) {
	var regionDisk entity.RegionDisk
	err := entity.NewRegionDiskQuerySet(tx).RegionIDEq(region.ID).DiskProfileIDEq(diskProfile.ID).CapacityEq(item.DiskCapacity).One(&regionDisk)
	if err == gorm.ErrRecordNotFound {
		regionDisk = entity.RegionDisk{
			Name:          string(diskProfile.Type),
			DisplayName:   string(diskProfile.Type),
			Capacity:      item.DiskCapacity,
			RegionID:      region.ID,
			DiskProfileID: diskProfile.ID,
		}
		if err = tx.Create(&regionDisk).Error; err != nil {
			return nil, err
		}
	}
	if err != nil {
		return nil, err
	}
	return &regionDisk, nil
}

func getOrCreateDiskProfile(tx *gorm.DB, diskType string) (*entity.DiskProfile, error) {
	var diskProfile entity.DiskProfile
	err := entity.NewDiskProfileQuerySet(tx).TypeEq(capacity.DiskType(diskType)).One(&diskProfile)
	if err == gorm.ErrRecordNotFound {
		diskProfile = entity.DiskProfile{
			Type: capacity.DiskType(diskType),
		}
		if err = tx.Create(&diskProfile).Error; err != nil {
			return nil, err
		}
	}
	if err != nil {
		return nil, err
	}
	return &diskProfile, nil
}

func tryUpdateRegionMachinePrice(tx *gorm.DB, regionMachine *entity.RegionMachine, item *ProviderComponentProfile) error {
	var regionMachinePrice entity.RegionMachinePrice
	err := entity.NewRegionMachinePriceQuerySet(tx).RegionMachineIDEq(regionMachine.ID).OrderDescByEffectedAt().One(&regionMachinePrice)
	if err != nil && err != gorm.ErrRecordNotFound {
		return err
	}
	if err == gorm.ErrRecordNotFound || item.Price != regionMachinePrice.Price {
		regionMachinePrice = entity.RegionMachinePrice{
			EffectedAt:      time.Now(),
			Price:           item.Price,
			RegionMachineID: regionMachine.ID,
		}
		if err = tx.Create(&regionMachinePrice).Error; err != nil {
			return err
		}
	}
	return nil
}

func getOrCreateRegionMachine(tx *gorm.DB, machineProfile *entity.MachineProfile, region *entity.Region) (*entity.RegionMachine, error) {
	var regionMachine entity.RegionMachine
	err := entity.NewRegionMachineQuerySet(tx).MachineProfileIDEq(machineProfile.ID).RegionIDEq(region.ID).One(&regionMachine)
	if err == gorm.ErrRecordNotFound {
		regionMachine = entity.RegionMachine{
			Name:             machineProfile.InstanceType.String(),
			DisplayName:      machineProfile.InstanceType.String(),
			RegionID:         region.ID,
			MachineProfileID: machineProfile.ID,
		}
		if err = tx.Create(&regionMachine).Error; err != nil {
			return nil, err
		}
	}
	return &regionMachine, nil
}

func getOrCreateMachineProfile(tx *gorm.DB, provider entity.Provider, item *ProviderComponentProfile) (*entity.MachineProfile, error) {
	var machineProfile entity.MachineProfile
	err := entity.NewMachineProfileQuerySet(tx).ProviderIDEq(provider.ID).InstanceTypeEq(capacity.MachineType(item.MachineType)).One(&machineProfile)
	if err == gorm.ErrRecordNotFound {
		machineProfile = entity.MachineProfile{
			MachineArch:  entity.MachineArchX86,
			InstanceType: capacity.MachineType(item.MachineType),
			ProviderID:   provider.ID,
			Memory:       item.Memory,
			CPU:          item.CPU,
		}
		err = tx.Create(&machineProfile).Error
		if err != nil {
			return nil, err
		}
	}
	return &machineProfile, nil
}
