package admin

import (
	"fmt"
	"time"

	"github.com/pingcap/dbaas/pkg/capacity"
	"github.com/pingcap/dbaas/pkg/tidb/component"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/central-sdk/db"
	"github.com/tidbcloud/central-sdk/db/dbutil"
	"github.com/tidbcloud/central-sdk/db/entity"
	clusterutil "github.com/tidbcloud/central-sdk/model/cluster"
	central "github.com/tidbcloud/central-sdk/proto/output"
	"gorm.io/gorm"
)

type TenantCluster struct {
	Id         entity.ID
	Name       string
	Provider   string
	Region     string
	Profile    string // FIXME: this filed seems not to be used now
	Version    string
	CreateTime time.Time
	Resources  []component.Resources
	Status     central.Cluster_Status
	ProjectID  entity.ID
}

type ClusterLifecycle struct {
	ID        uint64
	ProjectID uint64
	CreatedAt int64
	DeletedAt int64
}

// List all clusters of a tenant.
// By default only alive clusters are listed. This behavior can be changed by supplying additional modifiers.
func (m UserContext) ListTenantCluster(tenantID entity.ID, modifiers ...dbutil.ListClusterModifier) ([]*TenantCluster, error) {
	if m.tenantAndUser.ID == 0 {
		return nil, errcode.NewInvalidInputErr(errors.New("not a normal user"))
	}
	var (
		clusters []entity.Cluster
		err      error
	)
	err = db.Transaction(func(tx *gorm.DB) error {
		clusters, err = dbutil.ListClusters(tx, tenantID, modifiers...)
		return err
	})
	if err != nil {
		return nil, err
	}
	var internalItems []*TenantCluster
	for _, cluster := range clusters {
		var region *entity.Region
		var componentResource []component.Resources
		err = db.Transaction(func(tx *gorm.DB) error {
			_, region, err = dbutil.GetClusterProviderAndRegion(tx, cluster)
			if err != nil {
				return err
			}
			for _, cp := range cluster.Components {
				var machines []entity.ComponentMachine
				err := entity.NewComponentMachineQuerySet(tx).PreloadMachine().ComponentIDEq(cp.ID).All(&machines)
				if err != nil {
					return err
				}
				if len(machines) == 0 {
					return errors.Errorf("no any component machine for %s, cluster id: %d", cp.ComponentName, cp.ClusterID)
				}

				machine := machines[0]
				var machineProfile entity.MachineProfile
				err = entity.NewMachineProfileQuerySet(tx).IDEq(machine.Machine.MachineProfileID).One(&machineProfile)
				if err != nil {
					return err
				}

				replicas := len(machines)
				r := component.Resources{
					ComponentName: component.Name(cp.ComponentName),
					Image:         nil,
					Capacity: capacity.Capacity{
						MachineType: &machineProfile.InstanceType,
						Replicas:    uint32(replicas),
						Compute: &capacity.Compute{
							MemoryMi: machineProfile.Memory,
							CPU:      float32(machineProfile.CPU),
						},
					},
				}

				componentResource = append(componentResource, r)
			}
			return nil
		})
		internalItems = append(internalItems, m.buildInternalTenantCluster(tenantID, cluster, *region, componentResource))
	}
	return internalItems, nil
}

func (m UserContext) buildInternalTenantCluster(tenantID entity.ID, cluster entity.Cluster, region entity.Region, componentResources []component.Resources) *TenantCluster {
	c := &TenantCluster{
		Id:         cluster.ID,
		Name:       cluster.Name,
		Provider:   cluster.Provider.Name.String(),
		Region:     region.Name,
		CreateTime: cluster.CreatedAt,
		Resources:  componentResources,
		ProjectID:  cluster.ProjectID,
	}
	rx, err := m.runtimes.Context(cluster.ProjectID.Uint64(), cluster.Provider.Name, region.Name)
	if err != nil {
		//m.log.Debug("failed to get tidb cluster", zap.Uint64("clusterID", cluster.ID.Uint64()), zap.Error(err))
		return c
	}
	var ready bool
	if !cluster.Status.Creating() {
		ready = true
	}
	version, status, _ := clusterutil.CollectClusterRuntimeInfo(cluster, rx.Runtime, ready, m.log)
	c.Version = version
	c.Status = status
	return c
}

func (m UserContext) FindShootProxy(clusterID uint64) (string, error) {
	// TODO: Deduplicate with `findShootProxy` in `cluster.go`.
	var (
		cluster entity.Cluster
		region  *entity.Region
		err     error
	)
	err = db.Transaction(func(tx *gorm.DB) error {
		var err error
		cluster, err = dbutil.GetCluster(tx, entity.ID(clusterID))
		if err != nil {
			return err
		}

		region, err = dbutil.GetClusterRegion(tx, &cluster)
		if err != nil {
			return errcode.NewInternalErr(errors.Errorf("cannot get cluster region for %s", cluster.Name))
		}
		return nil
	})
	if err != nil {
		return "", err
	}
	rx, errCtx := m.runtimes.Context(cluster.ProjectID.Uint64(), cluster.Provider.Name, region.Name)
	if errCtx != nil {
		return "", errcode.NewInternalErr(errors.Errorf("target infra is unavailable"))
	}
	return rx.ProxyEndpoint, nil
}

func (m UserContext) DeleteTenantCluster(tenantID, clusterID uint64) error {
	return db.Transaction(func(tx *gorm.DB) error {
		var err error
		var cluster entity.Cluster
		if cluster, err = dbutil.GetCluster(tx, entity.ID(clusterID)); err != nil {
			return err
		}
		if cluster.TenantID.Uint64() != tenantID {
			return errcode.NewForbiddenErr(fmt.Errorf("cluster %d does not belong to tenant %d", clusterID, tenantID))
		}
		if err := dbutil.DeleteCluster(tx, entity.ID(clusterID)); err != nil {
			return err
		}
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.TenantID, m.tenantAndUser.Email, fmt.Sprintf("delete cluster %d for tenant %d", clusterID, tenantID))
	})
}

func (m UserContext) ListTenantClusterLifeCycle(tenantID uint64, start, end int64) ([]ClusterLifecycle, error) {
	var (
		clusters []entity.Cluster
		err      error
	)
	if err = db.Transaction(func(tx *gorm.DB) error {
		clusters, err = dbutil.GetClusterMatchTimeRange(tx, entity.ID(tenantID), start, end)
		return err
	}); err != nil {
		return nil, err
	}
	var clf []ClusterLifecycle
	for _, item := range clusters {
		var deletedAt int64 = 0
		if item.DeletedTime != nil {
			deletedAt = item.DeletedTime.Unix()
		}
		clf = append(clf, ClusterLifecycle{
			ID:        item.ID.Uint64(),
			ProjectID: item.ProjectID.Uint64(),
			CreatedAt: item.CreatedAt.Unix(),
			DeletedAt: deletedAt,
		})
	}
	return clf, nil
}

type ComponentScale struct {
	ComponentName string
	Replicas      uint32
}

func (m UserContext) ScaleTenantCluster(tenantID, clusterID uint64, scales []ComponentScale) error {
	scaleReq := dbutil.ScaleClusterComponents{
		ClusterID:    clusterID,
		AllowScaleIn: true,
	}
	for _, item := range scales {
		scaleReq.Components = append(scaleReq.Components, &dbutil.ComponentScale{
			ComponentName: item.ComponentName,
			Replicas:      item.Replicas,
		})
	}
	var (
		cluster entity.Cluster
		err     error
	)
	err = db.Transaction(func(tx *gorm.DB) error {
		if cluster, err = dbutil.GetCluster(tx, entity.ID(clusterID)); err != nil {
			return err
		}
		if cluster.TenantID.Uint64() != tenantID {
			return errcode.NewForbiddenErr(fmt.Errorf("cluster %d does not belong to tenant %d", clusterID, tenantID))
		}
		return nil
	})
	if err != nil {
		return errcode.NewInternalErr(err)
	}
	return clusterutil.ScaleComponent(m.log, m.tenantAndUser.Email, cluster.TenantID, cluster.ProjectID, scaleReq)
}
