package model

import (
	"fmt"
	"time"

	"github.com/pingcap/log"
	"go.uber.org/zap"
	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/o11y"
)

func ClusterCount() (int64, error) {
	var (
		total int64
		err   error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		total, err = db.NewClusterQuerySet(tx).Count()
		return err
	})

	return total, err
}

func AddCluster(c *db.Cluster) (*db.Cluster, error) {
	var (
		cluster db.Cluster
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		err = tx.Unscoped().Where("cluster_id=?", c.ClusterID).First(&cluster).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}
		if cluster.ID > 0 {
			return fmt.Errorf("duplicate cluster '%s'", c.ClusterID)
		}

		err = db.NewClusterQuerySet(tx).ClusterIDEq(c.ClusterID).One(&cluster)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				return c.Create(tx)
			}

			return err
		}

		return nil
	})

	if cluster.ClusterID == "" {
		cluster = *c
	}

	return &cluster, err
}

func AddClusterIfPresent(c *db.Cluster) (*db.Cluster, error) {
	var (
		cluster db.Cluster
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		err = tx.Unscoped().Where("cluster_id=?", c.ClusterID).First(&cluster).Error
		if err != nil && err != gorm.ErrRecordNotFound {
			return err
		}

		if cluster.ID > 0 {
			if !cluster.DeletedAt.Valid {
				log.Warn("duplicate cluster", zap.String("cluster_id", cluster.ClusterID))
				return nil
			}
			if err := tx.Unscoped().Where("cluster_id=?", cluster.ClusterID).Delete(&db.Cluster{}).Error; err != nil {
				return err
			}
		}
		err = db.NewClusterQuerySet(tx).ClusterIDEq(c.ClusterID).One(&cluster)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				return c.Create(tx)
			}

			return err
		}

		return nil
	})

	if cluster.ClusterID == "" {
		cluster = *c
	}

	return &cluster, err
}

func GetCluster(id string) (*db.Cluster, error) {
	var (
		cluster db.Cluster
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		return db.NewClusterQuerySet(tx).ClusterIDEq(id).One(&cluster)
	})

	return &cluster, err
}

func GetAnyCluster(id string) (*db.Cluster, error) {
	var (
		cluster db.Cluster
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		return db.NewClusterQuerySet(tx.Unscoped()).ClusterIDEq(id).One(&cluster)
	})

	return &cluster, err
}

func GetAnyClusterWithOrg(id string) (*db.Cluster, error) {
	var (
		cluster db.Cluster
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		return tx.Unscoped().Preload("Organization").Where("cluster_id = ?", id).First(&cluster).Error
	})

	return &cluster, err
}

func GetClusterWithOrg(clusterID string, orgID uint) (*db.Cluster, error) {
	var (
		cluster db.Cluster
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		return db.NewClusterQuerySet(tx.Unscoped()).ClusterIDEq(clusterID).OrgIDEq(orgID).One(&cluster)
	})

	return &cluster, err
}

func GetClusterWithDbaasIDs(dbaasClusterID string, dbaasOrgID uint) (*db.Cluster, error) {
	var (
		cluster db.Cluster
		err     error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		q := tx.
			Model(&db.Cluster{}).
			Joins("JOIN organizations ON organizations.id = clusters.org_id").
			Where("clusters.cluster_id = ? AND organizations.dbaas_org_id = ?", dbaasClusterID, dbaasOrgID)
		return q.First(&cluster).Error
	})

	return &cluster, err
}

func ClusterCountExceptCurrentClusterByOrgID(orgID uint, clusterID string) (int64, error) {
	var (
		count int64
		err   error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		count, err = db.NewClusterQuerySet(tx).ClusterIDNe(clusterID).OrgIDEq(orgID).Count()
		return err
	})

	return count, err
}

func LastUpdatedTime(orgID uint) (int, int64, error) {
	var (
		t     time.Time
		count int
	)
	err := db.Transaction(func(tx *gorm.DB) error {
		row := tx.Model(db.Cluster{}).Where("org_id = ?", orgID).Select("max(updated_at), count(*)").Row()
		return row.Scan(&t, &count)
	})
	if count != 0 {
		return count, t.Unix(), err
	}

	// no cluster in the org
	var org db.Organization
	err = db.Transaction(func(tx *gorm.DB) error {
		return tx.Model(db.Organization{}).Where("id = ?", orgID).First(&org).Error
	})
	if err != nil {
		return 0, 0, err
	}

	return 0, org.UpdatedAt.Unix(), nil
}

func DeleteCluster(clusterID string) ([]db.UploadFile, error) {
	files := []db.UploadFile{}
	err := db.Transaction(func(tx *gorm.DB) error {
		err := tx.Model(&db.UploadFile{}).Where("cluster_id = ?", clusterID).Find(&files).Error
		if err != nil {
			return err
		}
		err = tx.Model(&db.UploadFile{}).Where("cluster_id = ?", clusterID).Unscoped().Delete(files).Error
		if err != nil {
			log.Error("delete file failed, ", zap.Error(err))
			return err
		}
		_, err = db.NewClusterQuerySet(tx).ClusterIDEq(clusterID).DeleteNumUnscoped()
		return err
	})
	return files, err
}

func UpdateCluster(clusterID, name, description, status string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		updater := db.NewClusterQuerySet(tx).ClusterIDEq(clusterID).GetUpdater()
		if name != "" {
			updater.SetName(name)
		}

		if description != "" {
			updater.SetDesc(description)
		}

		if status != "" {
			updater.SetStatus(status)
		}

		return updater.Update()
	})
}

func UpdateClusterDeployType(deployTypeV2 string, clusterIds []string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		updater := db.NewClusterQuerySet(tx.Unscoped()).ClusterIDIn(clusterIds...).GetUpdater()
		updater.SetDeployTypeV2(deployTypeV2)
		return updater.Update()
	})
}

func MigrateDeployTypeV2FromDeployType(deployType, deployTypeV2 string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		updater := db.NewClusterQuerySet(tx.Unscoped()).DeployTypeEq(deployType).GetUpdater()
		updater.SetDeployTypeV2(deployTypeV2)
		return updater.Update()
	})
}

func ListNextNoParentIdGenClusters(page, pageSize int) ([]db.Cluster, error) {
	ret := make([]db.Cluster, 0, pageSize)
	err := db.Transaction(func(tx *gorm.DB) error {
		return tx.Unscoped().Model(&db.Cluster{}).
			Where("deploy_type in ? and (parent_id is null or parent_id = '')", []string{o11y.PremiumType, o11y.ByocType}).
			Order("cluster_id").Limit(pageSize).Offset((page - 1) * pageSize).Find(&ret).Error
	})
	return ret, err
}

func SetDefaultDeployTypeV2(deployType, deployTypeV2 string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Unscoped().Exec(`
		  UPDATE clusters SET deploy_type_v2 = ?
		  WHERE deploy_type = ? AND deploy_type_v2 IS NULL
		`, deployTypeV2, deployType).Error
	})
}

func MigrateServerlessBranchDeployType() error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Unscoped().Exec(`
			UPDATE clusters AS c
			JOIN clusters AS p ON c.parent_id = p.cluster_id
			SET c.deploy_type_v2 = p.deploy_type_v2
			WHERE c.deploy_type = 'shared' and c.parent_id != '' and c.parent_id IS NOT NULL
		`).Error
	})
}

func UpdateClusterParentId(clusterId, parentId string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		updater := db.NewClusterQuerySet(tx.Unscoped()).ClusterIDEq(clusterId).GetUpdater()
		updater.SetParentID(parentId)
		return updater.Update()
	})
}

func DeleteResourcePool(clusterID string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		updater := db.NewClusterQuerySet(tx).ClusterIDEq(clusterID).GetUpdater()
		now := timestamppb.Now()
		updater.SetDeletedAt(gorm.DeletedAt{
			Time:  now.AsTime(),
			Valid: true,
		})
		updater.SetEndTime(uint64(now.GetSeconds()))
		updater.SetStatus("deleted")
		return updater.Update()
	})
}

// TODO(shenjun): the sql may has time zone issue.
func GetExpiredClusters(expiredDay int) ([]db.Cluster, error) {
	var (
		clusters []db.Cluster
		err      error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		return db.NewClusterQuerySet(tx).UpdatedAtLt(time.Now().Add(-24 * time.Duration(expiredDay) * time.Hour)).All(&clusters)
	})

	return clusters, err
}

func ListRegions(providers []string) ([]string, error) {
	var regions []string

	query := db.GetDB().Model(&db.Cluster{})
	if len(providers) > 0 {
		query = query.Where("provider IN (?)", providers)
	}

	err := query.Unscoped().
		Select("DISTINCT region").
		Where("region is not null and region != ''").
		Order("region").
		Pluck("region", &regions).Error

	return regions, err
}

func MigrateClusterStatus(to string, from []string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		updater := db.NewClusterQuerySet(tx.Unscoped()).StatusIn(from...).GetUpdater()
		updater.SetStatus(to)
		return updater.Update()
	})
}
