package model

import (
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/errors"
	"github.com/tidbcloud/serverless-service/idl/pbgen/proto/rpc"
	mgmtv1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"gorm.io/gorm"

	"github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/datasource/clustersearch"
	"github.com/pingcap-inc/clinic/pkg/datasource/utils"
	"github.com/pingcap-inc/clinic/pkg/datasource/view"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/o11y"
	"github.com/pingcap-inc/clinic/pkg/pagination"
)

func InsertOrUpdateCloudCluster(c *mgmtv1.Cluster, forceUpdate bool) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		//  insert or update cluster
		var existCluster db.Cluster
		err := tx.Unscoped().Where("cluster_id  = ?", strconv.FormatUint(c.ClusterId, 10)).First(&existCluster).Error

		switch err {
		case gorm.ErrRecordNotFound:
			// 1. dbaas tenantID -> clinic orgID
			var org db.Organization
			var orgID uint
			err := tx.Unscoped().Where("dbaas_org_id  = ?", c.OrganizationId).First(&org).Error
			switch err {
			case gorm.ErrRecordNotFound:
				// insert a org without info if dbaas org is not updated on clinic
				err = tx.Model(&db.Organization{}).Create(&db.Organization{DbaasOrgID: uint(c.OrganizationId), OrgType: string(view.CloudDataSource)}).Error
				if err != nil {
					return err
				}
				qs := db.NewOrganizationQuerySet(tx)
				err = qs.DbaasOrgIDEq(uint(c.OrganizationId)).One(&org)
				if err != nil {
					return err
				}
				orgID = org.ID
			case nil:
				orgID = org.ID
			default:
				return err
			}

			cluster := utils.MgmtClusterToModel(orgID, c)
			utils.ApplyDevSharedClusterRegion(config.GetDeploymentEnv(), cluster)
			return tx.Unscoped().Model(&db.Cluster{}).Create(&cluster).Error
		case nil:
			cluster := utils.MgmtClusterToModel(0, c)
			utils.ApplyDevSharedClusterRegion(config.GetDeploymentEnv(), cluster)
			if forceUpdate || utils.ClusterSyncDiff(existCluster, *cluster) {
				return tx.Unscoped().Model(&cluster).Omit("OrgID").Updates(&cluster).Error
			}
			return nil
		default:
			return err
		}
	})
	return err
}

func InsertOrUpdateServerlessBranch(parentCluster *mgmtv1.Cluster, c *rpc.BasicBranch, forceUpdate bool) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		// 2. insert or update cluster
		var existCluster db.Cluster

		err := tx.Unscoped().Where("cluster_id  = ?", c.Id).First(&existCluster).Error
		switch err {
		case gorm.ErrRecordNotFound:

			// 1. dbaas tenantID -> clinic orgID
			var org db.Organization
			var orgID uint

			err := tx.Unscoped().Where("dbaas_org_id  = ?", parentCluster.OrganizationId).First(&org).Error
			switch err {
			case gorm.ErrRecordNotFound:
				// insert a org without info if dbaas org is not updated on clinic
				err = tx.Model(&db.Organization{}).Create(&db.Organization{DbaasOrgID: uint(parentCluster.OrganizationId), OrgType: string(view.CloudDataSource)}).Error
				if err != nil {
					return err
				}
				qs := db.NewOrganizationQuerySet(tx)
				err = qs.DbaasOrgIDEq(uint(parentCluster.OrganizationId)).One(&org)
				if err != nil {
					return err
				}
				orgID = org.ID
			case nil:
				orgID = org.ID
			default:
				return err
			}

			cluster := utils.BranchToModel(orgID, parentCluster, c)
			utils.ApplyDevSharedClusterRegion(config.GetDeploymentEnv(), &cluster)
			return tx.Unscoped().Model(&db.Cluster{}).Create(&cluster).Error
		case nil:
			cluster := utils.BranchToModel(0, parentCluster, c)
			utils.ApplyDevSharedClusterRegion(config.GetDeploymentEnv(), &cluster)
			if forceUpdate || cluster.UpdatedAt != existCluster.UpdatedAt {
				return tx.Unscoped().Model(&cluster).Omit("OrgID").Updates(&cluster).Error
			}
			return nil
		default:
			return err
		}
	})
	return err
}

func InsertOrUpdateNGCluster(c *db.Cluster, forceUpdate bool) error {
	utils.ApplyDevSharedClusterRegion(config.GetDeploymentEnv(), c)
	err := db.Transaction(func(tx *gorm.DB) error {
		//  insert or update cluster
		var existCluster db.Cluster
		err := tx.Unscoped().Where("cluster_id  = ?", c.ClusterID).First(&existCluster).Error

		switch err {
		case gorm.ErrRecordNotFound:
			// 1. dbaas tenantID -> clinic orgID
			var org db.Organization
			err := tx.Unscoped().Where("dbaas_org_id  = ? and org_type = ?", c.TenantID, string(view.CloudDataSource)).First(&org).Error
			switch err {
			case gorm.ErrRecordNotFound:
				// insert a org without info if dbaas org is not updated on clinic
				err = tx.Model(&db.Organization{}).Create(&db.Organization{DbaasOrgID: uint(c.TenantID), OrgType: string(view.CloudDataSource)}).Error
				if err != nil {
					return err
				}
				qs := db.NewOrganizationQuerySet(tx)
				err = qs.DbaasOrgIDEq(uint(c.TenantID)).One(&org)
				if err != nil {
					return err
				}
				c.OrgID = org.ID
			case nil:
				c.OrgID = org.ID
			default:
				return err
			}
			return tx.Unscoped().Model(&db.Cluster{}).Create(c).Error
		case nil:
			if forceUpdate || utils.ClusterSyncDiff(existCluster, *c) {
				return tx.Unscoped().Model(c).Omit("OrgID").Updates(c).Error
			}
			return nil
		default:
			return err
		}
	})
	return err
}

func UpdateBranches(clusterID uint64, branches []string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Unscoped().Model(&db.Cluster{}).Where("cluster_id = ?", clusterID).Update("branches", db.StringSlice(branches)).Error
	})
}

func SoftDeleteServerlessBranch(branchID string) error {
	return db.Transaction(func(tx *gorm.DB) error {
		return tx.Unscoped().Model(&db.Cluster{}).Where("cluster_id = ?", branchID).Updates(map[string]interface{}{
			"status":     "deleted",
			"deleted_at": gorm.DeletedAt{Time: time.Now(), Valid: true},
		}).Error
	})
}

func ListClusters(query string, orgID, projectID uint,
	page, perPage int,
	hideServerlessTier bool,
	hideDeleted bool,
) ([]db.Cluster, int64, error) {
	var (
		clusters []db.Cluster
		count    int64
		err      error
	)
	query = strings.TrimSpace(query)
	if err := clustersearch.RequirePolicyForQuery(query); err != nil {
		return nil, 0, err
	}

	err = db.Transaction(func(tx *gorm.DB) error {
		tx = tx.Unscoped().Model(&db.Cluster{}).Where("org_id = ?", strconv.FormatUint(uint64(orgID), 10))
		if query != "" {
			if !clustersearch.TextSearchPolicyAvailable() || !clustersearch.AllowTextSearch(orgID) {
				tx = tx.Where("cluster_id = ?", query)
			} else {
				tx = tx.Where("name like ? or cluster_id like ?", "%"+query+"%", "%"+query+"%")
			}
		}
		if projectID > 0 {
			tx = tx.Where("project_id = ?", strconv.FormatUint(uint64(projectID), 10))
		}
		if hideDeleted {
			tx = tx.Where("status NOT IN ?", []string{"deleted", "deletecomplete", "cleared"})
		}
		if hideServerlessTier {
			tx = tx.Where("deploy_type != ?", "shared")
		}
		if perPage > 0 {
			err = tx.Count(&count).Error
			if err != nil {
				return errors.Annotate(err, "count clusters error")
			}

			startIndex := pagination.GetStartIndex(page, perPage)
			tx = tx.Limit(perPage).Offset(startIndex)
		}

		if err := tx.Order("updated_at DESC").Order("id DESC").Find(&clusters).Error; err != nil {
			return errors.Annotate(err, "fetch clusters failed")
		}
		if perPage <= 0 {
			// no pagination
			count = int64(len(clusters))
		}

		return nil
	})
	if err != nil {
		return nil, 0, err
	}

	return clusters, count, nil
}

func ListRecoveringClusters() ([]db.Cluster, error) {
	var (
		clusters []db.Cluster
		err      error
	)
	err = db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Unscoped().
			Model(&db.Cluster{}).
			Where("status = ?", "recovering").
			Order("updated_at DESC").
			Order("id DESC").
			Find(&clusters).Error; err != nil {
			return errors.Annotate(err, "fetch clusters failed")
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	return clusters, nil
}

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

	err = db.Transaction(func(tx *gorm.DB) error {
		tx = tx.Unscoped().Model(&db.Cluster{}).Where("cluster_type = ?", string(view.CloudDataSource))

		err = tx.Count(&total).Error
		if err != nil {
			return errors.Annotate(err, "count clusters error")
		}
		return nil
	})

	return total, err
}

func ListCloudClusters(page, perPage int) ([]db.Cluster, error) {
	var (
		clusters []db.Cluster
		err      error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		tx = tx.Unscoped().Model(&db.Cluster{}).Where("cluster_type = ?", string(view.CloudDataSource))

		if perPage > 0 {
			startIndex := pagination.GetStartIndex(page, perPage)
			tx = tx.Limit(perPage).Offset(startIndex)
		}

		// updated_at is not unique, the pagination will skip some clusters
		// because MySQL is not promiss the order if the values are the same
		if err := tx.Order("cluster_id DESC").Find(&clusters).Error; err != nil {
			return errors.Annotate(err, "fetch clusters failed")
		}

		return nil
	})

	return clusters, err
}

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

	err = db.Transaction(func(tx *gorm.DB) error {
		tx = tx.Unscoped().Model(&db.Cluster{}).Where("deploy_type_v2 IN (?)", o11y.ResourcePoolTypes)

		err = tx.Count(&total).Error
		if err != nil {
			return errors.Annotate(err, "count clusters error")
		}
		return nil
	})

	return total, err
}

func ListResourcePoolClusters(page, perPage int) ([]db.Cluster, error) {
	var (
		clusters []db.Cluster
		err      error
	)

	err = db.Transaction(func(tx *gorm.DB) error {
		tx = tx.Unscoped().Model(&db.Cluster{}).Where("deploy_type_v2 IN (?)", o11y.ResourcePoolTypes)

		if perPage > 0 {
			startIndex := pagination.GetStartIndex(page, perPage)
			tx = tx.Limit(perPage).Offset(startIndex)
		}

		if err := tx.Order("cluster_id ASC").Find(&clusters).Error; err != nil {
			return errors.Annotate(err, "fetch clusters failed")
		}

		return nil
	})

	return clusters, err
}
