package repo_imp

import (
	"context"
	"strings"
	"time"

	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/pkgv2/log"
	"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"
	"github.com/tidbcloud/serverless-service/internal/errorcode"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type exportRepoImp struct {
	dbProvider db.TransProvider
}

func NewExportRepoImp(dbProvider db.TransProvider) repo.ExportRepo {
	return &exportRepoImp{dbProvider}
}

func (e *exportRepoImp) ListByFilter(
	ctx context.Context,
	filter entity.ExportFilter, //nolint:gocritic
) (exports []*entity.Export, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.ListByFilter")
	defer func() { tracing.EndSpan(span, err) }()

	rdb := e.dbProvider.ReadDB(ctx)
	rdb = buildExportFilter(filter, rdb)

	if err := rdb.Find(&exports).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "fail to list export")
	}
	return exports, nil
}

func (e *exportRepoImp) CountByFilter(
	ctx context.Context,
	filter entity.ExportFilter, //nolint:gocritic
) (count int64, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.CountByFilter")
	defer func() { tracing.EndSpan(span, err) }()

	rdb := e.dbProvider.ReadDB(ctx)
	rdb = buildExportCountFilter(filter, rdb)
	if err := rdb.Model(&entity.Export{}).Count(&count).Error; err != nil {
		return 0, bizerr.WrapGormError(err, "fail to count export")
	}
	return count, nil
}

//nolint:gocritic
func buildExportFilter(filter entity.ExportFilter, db *gorm.DB) *gorm.DB {
	if !filter.IncludeDeleted {
		db = db.Where("state != ?", constant.ExportStateDeleted)
	}
	if filter.TenantID != "" {
		db = db.Where("tenant_id = ?", filter.TenantID)
	}
	if filter.ClusterID != "" {
		db = db.Where("cluster_id = ?", filter.ClusterID)
	}
	if filter.Offset != nil {
		db = db.Offset(int(*filter.Offset))
	}
	if filter.PageSize != nil {
		db = db.Limit(int(*filter.PageSize))
	}
	if len(filter.States) > 0 {
		db = db.Where("state IN ?", filter.States)
	} else if filter.State != "" {
		db = db.Where("state = ?", filter.State)
	}
	if filter.HasClusterStatus != nil {
		if *filter.HasClusterStatus {
			db = db.Where("cluster_status_id <> 0")
		} else {
			db = db.Where("cluster_status_id = 0")
		}
	}
	if filter.CreatedAtAfter != nil {
		db = db.Where("created_at >= ?", filter.CreatedAtAfter)
	}
	if filter.OrderBy != "" {
		db = db.Order(filter.OrderBy)
	}
	if filter.NameLike != "" {
		db = db.Where("lower(name) like ?", "%"+strings.ToLower(filter.NameLike)+"%")
	}
	return db
}

//nolint:gocritic
func buildExportCountFilter(filter entity.ExportFilter, db *gorm.DB) *gorm.DB {
	// count filter need not handle page and order by
	if !filter.IncludeDeleted {
		db = db.Where("state != ?", constant.ExportStateDeleted)
	}
	if filter.TenantID != "" {
		db = db.Where("tenant_id = ?", filter.TenantID)
	}
	if filter.ClusterID != "" {
		db = db.Where("cluster_id = ?", filter.ClusterID)
	}
	if len(filter.States) > 0 {
		db = db.Where("state IN ?", filter.States)
	} else if filter.State != "" {
		db = db.Where("state = ?", filter.State)
	}
	if filter.HasClusterStatus != nil {
		if *filter.HasClusterStatus {
			db = db.Where("cluster_status_id <> 0")
		} else {
			db = db.Where("cluster_status_id = 0")
		}
	}
	if filter.CreatedAtAfter != nil {
		db = db.Where("create_at >= ?", filter.CreatedAtAfter)
	}
	if filter.NameLike != "" {
		db = db.Where("name like ?", "%"+filter.NameLike+"%")
	}
	return db
}

func (e *exportRepoImp) Create(ctx context.Context, export *entity.Export) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.Create")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := e.dbProvider.WriteDB(ctx)
	if err := wdb.Create(export).Error; err != nil {
		return bizerr.WrapGormError(err, "fail to create export")
	}
	return nil
}

func (e *exportRepoImp) TransitionPreparingState(
	ctx context.Context,
	export *entity.Export,
) (transitioned bool, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.TransitionPreparingState")
	defer func() { tracing.EndSpan(span, err) }()

	updates := map[string]any{
		"state":                    export.State,
		"encrypted_create_payload": "",
	}
	if export.State.IsFinalState() {
		now := time.Now().UTC()
		updates["completed_at"] = now
		export.CompletedAt = &now
	}
	if export.Reason != "" {
		updates["reason"] = export.Reason
	}

	result := e.dbProvider.WriteDB(ctx).Model(export).
		Where("id = ? and cluster_id = ? and state = ?", export.ID, export.ClusterID, constant.ExportStatePreparing).
		Updates(updates)
	if result.Error != nil {
		return false, bizerr.WrapGormError(result.Error, "fail to transition preparing export")
	}
	if result.RowsAffected == 0 {
		return false, nil
	}
	export.CreatePayload = ""
	return true, nil
}

func (e *exportRepoImp) GetById(
	ctx context.Context,
	clusterId, exportId string,
) (export *entity.Export, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.GetById")
	defer func() { tracing.EndSpan(span, err) }()

	rdb := e.dbProvider.ReadDB(ctx)
	export = &entity.Export{}
	if err := rdb.Where("id = ? and cluster_id = ? and state <> ?", exportId, clusterId, constant.ExportStateDeleted).First(export).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "fail to get export")
	}
	return export, nil
}

func (e *exportRepoImp) SoftDelete(ctx context.Context, export *entity.Export) (_ *entity.Export, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.SoftDelete")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := e.dbProvider.WriteDB(ctx)
	if err := wdb.Model(export).
		Where("id = ? and cluster_id = ? and state NOT IN ?", export.ID, export.ClusterID, []constant.ExportState{
			constant.ExportStatePreparing,
			constant.ExportStateRunning,
		}).
		Update("state", constant.ExportStateDeleted).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "fail to delete export")
	}
	return export, nil
}

func (e *exportRepoImp) UpdateRunningState(
	ctx context.Context,
	export *entity.Export,
) (_ *entity.Export, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.UpdateStatusById")
	defer func() { tracing.EndSpan(span, err) }()
	wdb := e.dbProvider.WriteDB(ctx)

	updateColumns := map[string]any{"state": export.State}
	if export.State.IsFinalState() {
		now := time.Now().UTC()
		updateColumns["completed_at"] = now
		if export.State == constant.ExportStateSucceed && export.TargetType == constant.ExportTargetTypeLocal {
			updateColumns["expired_at"] = now.Add(constant.ExportExpireTime)
		}
	}
	if export.Reason != "" {
		updateColumns["reason"] = export.Reason
	}
	if export.State == constant.ExportStateSucceed || export.State == constant.ExportStateCanceled {
		updateColumns["metering_status"] = constant.ExportMeteringStateRunning
	}
	result := wdb.Model(export).
		Where("id = ? and cluster_id = ? and state = ?", export.ID, export.ClusterID, constant.ExportStateRunning).
		Updates(updateColumns)
	if result.Error != nil {
		log.Info(ctx, "fail to update status", zap.Error(result.Error))
		return nil, bizerr.WrapGormError(result.Error, "fail to update status")
	}
	if result.RowsAffected == 0 {
		log.Info(ctx, "fail to update status for rowsAffected is 0")
		return nil, bizerr.New(
			errorcode.ErrCodeExportUpdateStateAffectedNoRows,
			"fail to update status for rowsAffected is 0",
		)
	}

	return export, nil
}

func (e *exportRepoImp) UpdateProxyEndpoint(
	ctx context.Context,
	exportID, clusterID, proxyEndpoint string,
) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.UpdateProxyEndpoint")
	defer func() { tracing.EndSpan(span, err) }()

	result := e.dbProvider.WriteDB(ctx).Model(&entity.Export{}).
		Where("id = ? and cluster_id = ? and (proxy_endpoint IS NULL or proxy_endpoint = '')", exportID, clusterID).
		Update("proxy_endpoint", proxyEndpoint)
	if result.Error != nil {
		return bizerr.WrapGormError(result.Error, "fail to update export proxy endpoint")
	}
	return nil
}

func (e *exportRepoImp) ClearClusterStatusID(
	ctx context.Context,
	exportID, clusterID string,
	clusterStatusID uint64,
) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.ClearClusterStatusID")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := e.dbProvider.WriteDB(ctx)
	if err := wdb.Model(&entity.Export{}).
		Where("id = ? and cluster_id = ? and cluster_status_id = ?", exportID, clusterID, clusterStatusID).
		Update("cluster_status_id", 0).Error; err != nil {
		return bizerr.WrapGormError(err, "fail to clear cluster status id")
	}
	return nil
}

func (e *exportRepoImp) ListMetering(ctx context.Context) (exports []*entity.Export, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.ListMetering")
	defer func() { tracing.EndSpan(span, err) }()

	rdb := e.dbProvider.ReadDB(ctx)

	// only list the exports that metering status is running and metering_times < max_metering_times
	rdb = rdb.Where("metering_status = ?", constant.ExportMeteringStateRunning)
	rdb = rdb.Where("metering_times < ?", constant.ExportMaxMeteringTimes)

	if err := rdb.Find(&exports).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "fail to list metering export")
	}
	return exports, nil
}

func (e *exportRepoImp) UpdateMeteringStatus(ctx context.Context, export *entity.Export) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.UpdateMeteringStatus")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := e.dbProvider.WriteDB(ctx)
	if err := wdb.Model(export).
		Where("id = ? and cluster_id = ?", export.ID, export.ClusterID).
		Update("metering_status", export.MeteringStatus).Error; err != nil {
		return bizerr.WrapGormError(err, "fail to update metering status")
	}
	return nil
}

func (e *exportRepoImp) IncreaseMeteringCount(ctx context.Context, export *entity.Export) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.IncreaseMeteringStatus")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := e.dbProvider.WriteDB(ctx)
	if err := wdb.Model(export).
		Where("id = ? and cluster_id = ?", export.ID, export.ClusterID).
		Update("metering_times", export.MeteringTimes+1).Error; err != nil {
		return bizerr.WrapGormError(err, "fail to update metering status")
	}
	return nil
}

func (e *exportRepoImp) GroupByState(ctx context.Context) (_ []*entity.ExportStateCount, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "exportRepo.GroupByState")
	defer func() { tracing.EndSpan(span, err) }()

	rdb := e.dbProvider.ReadDB(ctx)
	var result []*entity.ExportStateCount
	if err := rdb.Model(&entity.Export{}).Select("state, count(*) as count").Group("state").Scan(&result).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "fail to count by state")
	}
	return result, nil
}
