package repo_imp

import (
	"context"
	"time"

	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/pkgv2/tracing"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/entity"
	"github.com/tidbcloud/serverless-service/internal/domain/repo"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

type clusterRepoImp struct {
	dbProvider db.Provider
}

func NewClusterRepo(
	dbProvider db.Provider,
) repo.ClusterRepo {
	return &clusterRepoImp{dbProvider}
}

func buildTxByClusterFilter(tx *gorm.DB, filter *repo.ClusterFilter) *gorm.DB {
	tx = tx.Model(&entity.Cluster{})
	if filter == nil {
		return tx
	}

	if len(filter.TenantIds) > 0 {
		tx = tx.Where("tenant_id IN ?", filter.TenantIds)
	}
	if len(filter.ProjectIds) > 0 {
		tx = tx.Where("project_id IN ?", filter.ProjectIds)
	}
	if len(filter.ClusterIds) > 0 {
		tx = tx.Where("id IN ?", filter.ClusterIds)
	}
	if len(filter.Regions) > 0 {
		tx = tx.Where("region_name IN ?", filter.Regions)
	}
	if len(filter.RuModes) > 0 {
		tx = tx.Where("ru_mode IN ?", filter.RuModes)
	}
	if filter.IsPaid != nil {
		tx = tx.Where("is_paid=?", *filter.IsPaid)
	}
	return tx
}

func (r *clusterRepoImp) ListClustersInPage(
	ctx context.Context,
	filter *repo.ClusterFilter,
	page, size uint32,
) ([]*entity.Cluster, bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterRepo.ListClustersInPage")
	defer func() { tracing.EndSpan(span, nil) }()

	var clusters []*entity.Cluster
	rdb := r.dbProvider.ReadDB(ctx)
	rdb = buildTxByClusterFilter(rdb, filter)

	offset := int((page - 1) * size)
	if e := rdb.Offset(offset).Limit(int(size)).Find(&clusters).Error; e != nil {
		return nil, bizerr.WrapGormError(e, "failed to list clusters in page")
	}
	return clusters, nil
}

func (r *clusterRepoImp) CountClusters(ctx context.Context, filter *repo.ClusterFilter) (int64, bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterRepo.CountClustersByPaid")
	defer func() { tracing.EndSpan(span, nil) }()

	var count int64
	rdb := r.dbProvider.ReadDB(ctx)
	rdb = buildTxByClusterFilter(rdb, filter)
	if e := rdb.Count(&count).Error; e != nil {
		return 0, bizerr.WrapGormError(e, "failed to count clusters")
	}

	return count, nil
}

func (r *clusterRepoImp) GetClusterByID(
	ctx context.Context,
	clusterID uint64,
) (_ *entity.Cluster, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterRepo.GetCluster")
	defer func() { tracing.EndSpan(span, err) }()

	var cluster entity.Cluster
	rdb := r.dbProvider.ReadDB(ctx)
	if e := rdb.Model(&entity.Cluster{}).
		Where("id=?", clusterID).First(&cluster).Error; e != nil {
		return nil, bizerr.WrapGormError(e, "failed to find cluster %d", clusterID)
	}

	return &cluster, nil
}

func (r *clusterRepoImp) UpdateRUMode(
	ctx context.Context,
	clusterID uint64,
	mode constant.ClusterRUMode,
) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterRepo.UpdateRUMode")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := r.dbProvider.WriteDB(ctx)
	if e := wdb.Model(&entity.Cluster{}).
		Where("id=?", clusterID).Update("ru_mode", mode).Error; e != nil {
		return bizerr.WrapGormError(e, "failed to update Cluster %d ru_mode to %s", clusterID, mode)
	}
	return nil
}

func (r *clusterRepoImp) Save(ctx context.Context, cluster *entity.Cluster) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterRepo.Save")
	defer func() { tracing.EndSpan(span, err) }()

	if cluster == nil {
		return nil
	}

	wdb := r.dbProvider.WriteDB(ctx)
	if e := wdb.Save(cluster).Error; e != nil {
		return bizerr.WrapGormError(e, "failed to save Cluster %d", cluster.ID)
	}
	return nil
}

func (r *clusterRepoImp) Remove(ctx context.Context, clusterID uint64) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterRepo.Remove")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := r.dbProvider.WriteDB(ctx)
	if e := wdb.Where("id=?", clusterID).Delete(&entity.Cluster{}).Error; e != nil {
		return bizerr.WrapGormError(e, "failed to remove Cluster %d", clusterID)
	}
	return nil
}

type clusterTidbPoolRepoImp struct {
	dbProvider db.Provider
}

func NewClusterTidbPoolRepo(
	dbProvider db.Provider,
) repo.ClusterTidbPoolRepo {
	return &clusterTidbPoolRepoImp{dbProvider}
}

func (r *clusterTidbPoolRepoImp) ListSpendLimitEvents(
	ctx context.Context, clusterID uint64, start, end time.Time,
) ([]*entity.ClusterTidbPool, bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterTidbPoolRepo.ListSpendLimitEvents")
	defer func() { tracing.EndSpan(span, nil) }()

	var events []*entity.ClusterTidbPool
	rdb := r.dbProvider.ReadDB(ctx)
	if e := rdb.Model(&entity.ClusterTidbPool{}).
		Where("cluster_id=? AND type=? AND status=? AND created_at BETWEEN ? AND ?",
			clusterID, constant.TypeSpendLimit, constant.StatusSynced, start, end).
		Order(clause.OrderByColumn{
			Column: clause.Column{Name: "created_at"},
			Desc:   false,
		}).
		Find(&events).Error; e != nil {
		return nil, bizerr.WrapGormError(e, "failed to find Spending Limit events")
	}

	return events, nil
}

func (r *clusterTidbPoolRepoImp) LoadUnsyncedRecords(
	ctx context.Context,
) (_ []*entity.ClusterTidbPool, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterTidbPoolRepo.LoadUnsyncedRecords")
	defer func() { tracing.EndSpan(span, err) }()

	var records []*entity.ClusterTidbPool
	rdb := r.dbProvider.ReadDB(ctx)
	if e := rdb.Model(&entity.ClusterTidbPool{}).
		Where("status=?", constant.StatusUnsynced).
		Order(clause.OrderByColumn{
			Column: clause.Column{Name: "created_at"},
			Desc:   false,
		}).
		Find(&records).Error; e != nil {
		return nil, bizerr.WrapGormError(e, "failed to find unsynced ClusterTidbPool")
	}

	return records, nil
}

func (r *clusterTidbPoolRepoImp) Save(ctx context.Context, record *entity.ClusterTidbPool) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterTidbPoolRepo.Save")
	defer func() { tracing.EndSpan(span, err) }()

	if record == nil {
		return nil
	}

	record.Status = constant.StatusUnsynced
	wdb := r.dbProvider.WriteDB(ctx)
	if e := wdb.Save(record).Error; e != nil { // updated_at will be updated automatically by gorm
		return bizerr.WrapGormError(e, "failed to save ClusterTidbPool of cluster %d", record.ClusterID)
	}

	return nil
}

func (r *clusterTidbPoolRepoImp) MarkAsSynced(
	ctx context.Context,
	record *entity.ClusterTidbPool,
) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterTidbPoolRepo.MarkAsSynced")
	defer func() { tracing.EndSpan(span, err) }()

	if record == nil {
		return nil
	}

	record.Status = constant.StatusSynced
	wdb := r.dbProvider.WriteDB(ctx)
	if e := wdb.Save(record).Error; e != nil {
		return bizerr.WrapGormError(e, "failed to mark ClusterTidbPool of cluster %d as synced", record.ClusterID)
	}

	return nil
}

func (r *clusterTidbPoolRepoImp) Remove(ctx context.Context, eventID uint64) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterTidbPoolRepo.Remove")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := r.dbProvider.WriteDB(ctx)
	if e := wdb.Where("event_id=?", eventID).Delete(&entity.ClusterTidbPool{}).Error; e != nil {
		return bizerr.WrapGormError(e, "failed to remove ClusterTidbPool of event %d", eventID)
	}

	return nil
}

func (r *clusterTidbPoolRepoImp) RemoveAllEvents(ctx context.Context, clusterID uint64) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterTidbPoolRepo.Remove")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := r.dbProvider.WriteDB(ctx)
	if e := wdb.Where("cluster_id=?", clusterID).Delete(&entity.ClusterTidbPool{}).Error; e != nil {
		return bizerr.WrapGormError(e, "failed to remove ClusterTidbPool of cluster %d", clusterID)
	}

	return nil
}

func (r *clusterTidbPoolRepoImp) CleanUp(ctx context.Context, before time.Time, limit int) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterTidbPoolRepo.CleanUp")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := r.dbProvider.WriteDB(ctx)
	if e := wdb.Where("updated_at<?", before).Where("status=?", constant.StatusSynced).Limit(limit).Delete(&entity.ClusterTidbPool{}).Error; e != nil {
		return bizerr.WrapGormError(e, "failed to cleanup ClusterTidbPool")
	}

	return nil
}

func (r *clusterTidbPoolRepoImp) Query(
	ctx context.Context,
	sql string,
) (records []*entity.ClusterTidbPool, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "clusterTidbPoolRepo.Query")
	defer func() { tracing.EndSpan(span, err) }()
	rdb := r.dbProvider.ReadDB(ctx)
	if e := rdb.Raw(sql).Find(&records).Error; e != nil {
		return nil, bizerr.WrapGormError(e, "failed to query ClusterTidbPool")
	}

	return records, nil
}

type downtimeRepoImp struct {
	dbProvider db.Provider
}

func NewDowntimeRepo(
	dbProvider db.Provider,
) repo.DowntimeRepo {
	return &downtimeRepoImp{dbProvider}
}

func (r *downtimeRepoImp) Save(ctx context.Context, record *entity.Downtime) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "downtimeRepo.Save")
	defer func() { tracing.EndSpan(span, err) }()

	if record == nil {
		return nil
	}

	wdb := r.dbProvider.WriteDB(ctx)
	if e := wdb.Save(record).Error; e != nil {
		return bizerr.WrapGormError(e, "failed to save Downtime of cluster %s", record.ClusterID)
	}

	return nil
}

func (r *downtimeRepoImp) MultiSave(ctx context.Context, records []*entity.Downtime) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "downtimeRepo.Save")
	defer func() { tracing.EndSpan(span, err) }()

	if records == nil {
		return nil
	}

	wdb := r.dbProvider.WriteDB(ctx)
	if e := wdb.Save(records).Error; e != nil {
		return bizerr.WrapGormError(e, "failed to save records %v", records)
	}

	return nil
}

func (r *downtimeRepoImp) ListByFilter(
	ctx context.Context,
	filter *repo.DowntimeFilter,
) ([]*entity.Downtime, bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "downtimeRepo.ListByFilter")
	defer func() { tracing.EndSpan(span, nil) }()

	var downtimes []*entity.Downtime
	rdb := r.dbProvider.ReadDB(ctx)
	rdb = buildTxByDowntimeFilter(rdb, filter)

	if e := rdb.Find(&downtimes).Error; e != nil {
		return nil, bizerr.WrapGormError(e, "failed to list downtimes")
	}
	return downtimes, nil
}

func buildTxByDowntimeFilter(tx *gorm.DB, filter *repo.DowntimeFilter) *gorm.DB {
	tx = tx.Model(&entity.Downtime{})
	if filter == nil {
		return tx
	}
	if len(filter.ClusterIDs) > 0 {
		tx = tx.Where("cluster_id IN ?", filter.ClusterIDs)
	}
	if len(filter.Regions) > 0 {
		tx = tx.Where("region IN ?", filter.Regions)
	}
	if len(filter.Months) > 0 {
		tx = tx.Where("id IN ?", filter.Months)
	}
	return tx
}
