package repo_imp

import (
	"context"

	"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"
)

type dmRepoImp struct {
	dbProvider db.Provider
}

func NewDMTaskRepo(dbProvider db.Provider) repo.DMTaskRepo {
	return &dmRepoImp{dbProvider}
}

func buildDMTaskFilter(filter *entity.DMTaskFilter, db *gorm.DB, applyPagination bool) (*gorm.DB, bizerr.BizError) {
	if filter == nil {
		return nil, bizerr.New(bizerr.ErrCodeDB, "filter is required")
	}
	if filter.ClusterID != "" {
		db = db.Where("cluster_id = ?", filter.ClusterID)
	}
	if filter.TenantID != "" {
		db = db.Where("tenant_id = ?", filter.TenantID)
	}
	if len(filter.Stages) > 0 {
		db = db.Where("stage in ?", filter.Stages)
	}
	if filter.FailedTimeGTE != nil {
		db = db.Where("failed_time >= ?", *filter.FailedTimeGTE)
	}
	if filter.OrderBy != "" {
		db = db.Order(filter.OrderBy)
	}
	if applyPagination {
		if filter.Offset != nil {
			db = db.Offset(*filter.Offset)
		}
		if filter.Limit != nil {
			db = db.Limit(*filter.Limit)
		}
	}
	return db, nil
}

func (r *dmRepoImp) ListTasks(
	ctx context.Context,
	filter *entity.DMTaskFilter,
) (tasks []*entity.DMTask, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.ListTasks")
	defer func() { tracing.EndSpan(span, err) }()

	rdb := r.dbProvider.ReadDB(ctx)
	rdb, err = buildDMTaskFilter(filter, rdb, true)
	if err != nil {
		return nil, err
	}
	if err := rdb.Model(&entity.DMTask{}).Preload("DMSources").Find(&tasks).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to list DMTasks")
	}
	return tasks, nil
}

func (r *dmRepoImp) GetTask(
	ctx context.Context,
	clusterID,
	taskID string,
	stages []constant.DMTaskStage,
) (task *entity.DMTask, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.GetTask")
	defer func() { tracing.EndSpan(span, err) }()

	rdb := r.dbProvider.ReadDB(ctx)
	task = &entity.DMTask{}
	query := rdb.Model(&entity.DMTask{}).Preload("DMSources").Where("id = ? AND cluster_id = ?", taskID, clusterID)
	if len(stages) > 0 {
		query = query.Where("stage IN ?", stages)
	}
	if err := query.First(task).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to get DMTask, id: %s", taskID)
	}
	return task, nil
}

func (r *dmRepoImp) CreateTask(ctx context.Context, task *entity.DMTask) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.CreateTask")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := r.dbProvider.WriteDB(ctx)
	if err := wdb.Create(task).Error; err != nil {
		return bizerr.WrapGormError(err, "failed to create DMTask")
	}
	return nil
}

func (r *dmRepoImp) CountTasks(ctx context.Context, filter *entity.DMTaskFilter) (_ int64, bizErr bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.CountTasks")
	defer func() { tracing.EndSpan(span, bizErr) }()

	rdb := r.dbProvider.ReadDB(ctx)
	// build filter WITHOUT applying pagination (offset/limit) so Count returns total matching rows
	rdb, bizErr = buildDMTaskFilter(filter, rdb, false)
	if bizErr != nil {
		return 0, bizErr
	}
	var count int64
	if err := rdb.Model(&entity.DMTask{}).Count(&count).Error; err != nil {
		return 0, bizerr.WrapGormError(err, "failed to count DMTasks")
	}
	return count, nil
}

// UpdateTask updates specified columns of a DMTask by id.
func (r *dmRepoImp) UpdateTask(
	ctx context.Context,
	id string,
	stages []constant.DMTaskStage,
	updates map[string]any,
) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.UpdateTask")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := r.dbProvider.WriteDB(ctx)
	query := wdb.Model(&entity.DMTask{}).
		Where("id = ?", id)
	if len(stages) > 0 {
		query = query.Where("stage IN ?", stages)
	}
	result := query.Updates(updates)
	if result.Error != nil {
		return bizerr.WrapGormError(result.Error, "failed to update DMTask, id: %s", id)
	}
	return nil
}

func (r *dmRepoImp) UpdateSource(ctx context.Context, id string, updates map[string]any) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.UpdateSource")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := r.dbProvider.WriteDB(ctx)
	result := wdb.Model(&entity.DMSource{}).
		Where("id = ? AND stage <> ?", id, constant.DMSourceStageDeleted).
		Updates(updates)
	if result.Error != nil {
		return bizerr.WrapGormError(result.Error, "failed to update DMSource, id: %s", id)
	}
	return nil
}

func (r *dmRepoImp) CreatePrecheck(ctx context.Context, precheck *entity.DMPrecheck) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.CreatePrecheck")
	defer func() { tracing.EndSpan(span, err) }()

	if precheck == nil {
		return bizerr.New(bizerr.ErrCodeParams, "precheck is nil")
	}
	wdb := r.dbProvider.WriteDB(ctx)
	if err := wdb.Create(precheck).Error; err != nil {
		return bizerr.New(bizerr.ErrCodeDB, "failed to create DMPrecheck: %v", err)
	}
	return nil
}

func buildDMPrecheckFilter(filter *entity.DMPrecheckFilter, db *gorm.DB) (*gorm.DB, bizerr.BizError) {
	if filter == nil {
		return nil, bizerr.New(bizerr.ErrCodeDB, "filter is required")
	}
	db = db.Where("canceled_time IS NULL")
	if filter.ClusterID != "" {
		db = db.Where("cluster_id = ?", filter.ClusterID)
	}
	if filter.TenantID != "" {
		db = db.Where("tenant_id = ?", filter.TenantID)
	}
	if len(filter.Stages) > 0 {
		db = db.Where("stage in ?", filter.Stages)
	}
	if filter.OrderBy != "" {
		db = db.Order(filter.OrderBy)
	}
	if filter.Offset != nil {
		db = db.Offset(*filter.Offset)
	}
	if filter.Limit != nil {
		db = db.Limit(*filter.Limit)
	}
	return db, nil
}

func (r *dmRepoImp) ListPrechecks(
	ctx context.Context,
	filter *entity.DMPrecheckFilter,
) (prechecks []*entity.DMPrecheck, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.ListPrechecks")
	defer func() { tracing.EndSpan(span, err) }()

	rdb := r.dbProvider.ReadDB(ctx)
	rdb, err = buildDMPrecheckFilter(filter, rdb)
	if err != nil {
		return nil, err
	}
	if err := rdb.Model(&entity.DMPrecheck{}).Preload("DMPrecheckItems").Find(&prechecks).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to list DMPrechecks")
	}
	return prechecks, nil
}

func (r *dmRepoImp) GetPrecheck(
	ctx context.Context,
	clusterID, id string,
) (precheck *entity.DMPrecheck, err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.GetPrecheck")
	defer func() { tracing.EndSpan(span, err) }()

	rdb := r.dbProvider.ReadDB(ctx)
	precheck = &entity.DMPrecheck{}
	if err := rdb.Model(&entity.DMPrecheck{}).Preload("DMPrecheckItems").Where("id = ? AND cluster_id = ?", id, clusterID).First(precheck).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to get DMPrecheck, id: %s", id)
	}
	return precheck, nil
}

func (r *dmRepoImp) UpdatePrecheck(ctx context.Context, id string, updates map[string]any) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.UpdatePrecheck")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := r.dbProvider.WriteDB(ctx)
	excluded := []constant.DMPrecheckStage{constant.DMPrecheckStageFailed, constant.DMPrecheckStageCanceled}
	result := wdb.Model(&entity.DMPrecheck{}).Where("id = ? AND stage NOT IN ?", id, excluded).Updates(updates)
	if result.Error != nil {
		return bizerr.WrapGormError(result.Error, "failed to update DMPrecheck, id: %s", id)
	}
	return nil
}

func (r *dmRepoImp) CreatePrecheckItem(ctx context.Context, item *entity.DMPrecheckItem) (bizErr bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.CreatePrecheckItem")
	defer func() { tracing.EndSpan(span, bizErr) }()

	wdb := r.dbProvider.WriteDB(ctx)
	err := wdb.Create(item).Error
	if err != nil {
		return bizerr.WrapGormError(err, "failed to add item to DMPrecheck")
	}

	return nil
}

// CreateVpcEndpoint persists a DMVpcEndpoint record.
func (r *dmRepoImp) CreateVpcEndpoint(ctx context.Context, endpoint *entity.DMVpcEndpoint) (bizErr bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.CreateVpcEndpoint")
	defer func() { tracing.EndSpan(span, bizErr) }()

	if endpoint == nil {
		return bizerr.New(bizerr.ErrCodeParams, "endpoint is nil")
	}
	wdb := r.dbProvider.WriteDB(ctx)
	if err := wdb.Create(endpoint).Error; err != nil {
		return bizerr.WrapGormError(err, "failed to create DMVpcEndpoint")
	}
	return nil
}

// buildVpcEndpointFilter builds the query for listing VpcEndpoints based on the given filter.
func buildVpcEndpointFilter(filter *entity.VpcEndpointFilter, db *gorm.DB) (*gorm.DB, bizerr.BizError) {
	if filter == nil {
		return nil, bizerr.New(bizerr.ErrCodeDB, "filter is required")
	}
	if filter.ClusterID != "" {
		db = db.Where("cluster_id = ?", filter.ClusterID)
	}
	if len(filter.States) > 0 {
		db = db.Where("state IN ?", filter.States)
	}
	if filter.CreatedBefore != nil {
		db = db.Where("created_at < ?", *filter.CreatedBefore)
	}
	if filter.EmptyTaskID {
		db = db.Where("dm_task_id = ''")
	}
	if len(filter.TaskStageIn) > 0 {
		// Join with dm_tasks to filter by DM task stage
		db = db.Joins("JOIN dm_tasks ON dm_tasks.id = dm_vpc_endpoints.dm_task_id").
			Where("dm_tasks.stage IN ?", filter.TaskStageIn)
	}
	if filter.OrderBy != "" {
		db = db.Order(filter.OrderBy)
	}
	if filter.Offset != nil {
		db = db.Offset(*filter.Offset)
	}
	if filter.Limit != nil {
		db = db.Limit(*filter.Limit)
	}
	return db, nil
}

// GetVpcEndpoint returns a DMVpcEndpoint by cluster id and endpoint id.
func (r *dmRepoImp) GetVpcEndpoint(
	ctx context.Context,
	clusterID, id string,
) (endpoint *entity.DMVpcEndpoint, bizErr bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.GetVpcEndpoint")
	defer func() { tracing.EndSpan(span, bizErr) }()

	rdb := r.dbProvider.ReadDB(ctx)
	endpoint = &entity.DMVpcEndpoint{}
	if err := rdb.Model(&entity.DMVpcEndpoint{}).
		Where("cluster_id = ? AND id = ? AND state <> ?", clusterID, id, constant.DMVpcEndpointStateDeleted).
		First(endpoint).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to get VpcEndpoint")
	}
	return endpoint, nil
}

// UpdateVpcEndpoint creates or updates a DMVpcEndpoint record by cluster_id + endpoint_id.
func (r *dmRepoImp) UpdateVpcEndpoint(ctx context.Context, id string, updates map[string]any) (bizErr bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.UpdateVpcEndpoint")
	defer func() { tracing.EndSpan(span, bizErr) }()
	if updates == nil {
		return bizerr.New(bizerr.ErrCodeParams, "updates is nil")
	}
	wdb := r.dbProvider.WriteDB(ctx)

	// preserve exclusion semantics similar to UpdatePrecheck
	excluded := []constant.DMVpcEndpointState{constant.DMVpcEndpointStateFailed, constant.DMVpcEndpointStateDeleted}
	result := wdb.Model(&entity.DMVpcEndpoint{}).
		Where("id = ? AND state NOT IN ?", id, excluded).
		Updates(updates)
	if result.Error != nil {
		return bizerr.WrapGormError(result.Error, "failed to update DMVpcEndpoint, id: %s", id)
	}
	return nil
}

// ListVpcEndpoints returns all DMVpcEndpoint records that are not soft-deleted.
func (r *dmRepoImp) ListVpcEndpoints(
	ctx context.Context,
	filter *entity.VpcEndpointFilter,
) (endpoints []*entity.DMVpcEndpoint, bizErr bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.ListVpcEndpoints")
	defer func() { tracing.EndSpan(span, bizErr) }()

	rdb := r.dbProvider.ReadDB(ctx)
	endpoints = []*entity.DMVpcEndpoint{}
	var q *gorm.DB
	if q, bizErr = buildVpcEndpointFilter(filter, rdb.Model(&entity.DMVpcEndpoint{})); bizErr != nil {
		return nil, bizErr
	}
	if err := q.Find(&endpoints).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to list DMVpcEndpoint")
	}
	return endpoints, nil
}

// UpdatePrecheckItem updates specified columns of a DMPrecheckItem by id.
func (r *dmRepoImp) UpdatePrecheckItem(ctx context.Context, id string, updates map[string]any) (err bizerr.BizError) {
	ctx, span := Tracer.Start(ctx, "dmRepoImp.UpdatePrecheckItem")
	defer func() { tracing.EndSpan(span, err) }()

	wdb := r.dbProvider.WriteDB(ctx)
	excluded := []constant.DMPrecheckStage{constant.DMPrecheckStageFailed, constant.DMPrecheckStageCanceled}
	result := wdb.Model(&entity.DMPrecheckItem{}).Where("id = ? AND stage NOT IN ?", id, excluded).Updates(updates)
	if result.Error != nil {
		return bizerr.WrapGormError(result.Error, "failed to update DMPrecheckItem, id: %s", id)
	}
	return nil
}
