package repository

import (
	"context"
	"time"

	"github.com/tidbcloud/pkgv2/db"
	"google.golang.org/protobuf/types/known/timestamppb"

	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/repository"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/infra/repository/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
)

type DataSynchronizationRepoImpl struct {
	dbProvider db.Provider
}

func NewDataSynchronizationRepo(dbProvider db.Provider) repository.DataSynchronizationRepo {
	return &DataSynchronizationRepoImpl{dbProvider: dbProvider}
}

func (r *DataSynchronizationRepoImpl) Create(ctx context.Context, ds *entity.DataSynchronization) error {
	ctx, span := tracer.Start(ctx, "DataSynchronizationRepo.Create")
	defer span.End()

	if ds == nil {
		return bizerr.New(bizerr.ErrCodeParams, "data synchronization is nil")
	}

	wdb := r.dbProvider.WriteDB(ctx)
	m := cvtDataSynchronizationEntityToModel(ds)
	if err := wdb.Create(m).Error; err != nil {
		return bizerr.WrapGormError(err, "create data synchronization failed")
	}

	// Re-load to get DB-populated timestamps.
	var created schema.DataSynchronization
	if err := wdb.Where("id = ?", m.ID).First(&created).Error; err == nil {
		m = &created
	}

	ds.Id = uint64(m.ID)
	ds.CreateTime = toProtoTime(m.CreatedAt)
	ds.UpdateTime = toProtoTime(m.UpdatedAt)
	return nil
}

func (r *DataSynchronizationRepoImpl) Get(ctx context.Context, id uint64) (*entity.DataSynchronization, error) {
	ctx, span := tracer.Start(ctx, "DataSynchronizationRepo.Get")
	defer span.End()

	if id == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "id is required")
	}

	rdb := r.dbProvider.ReadDB(ctx)
	var m schema.DataSynchronization
	if err := rdb.Where("id = ?", id).First(&m).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "get data synchronization failed")
	}

	return cvtDataSynchronizationModelToEntity(&m), nil
}

func (r *DataSynchronizationRepoImpl) Update(ctx context.Context, ds *entity.DataSynchronization) error {
	ctx, span := tracer.Start(ctx, "DataSynchronizationRepo.Update")
	defer span.End()

	if ds == nil {
		return bizerr.New(bizerr.ErrCodeParams, "data synchronization is nil")
	}
	if ds.Id == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "id is required")
	}

	wdb := r.dbProvider.WriteDB(ctx)
	m := cvtDataSynchronizationEntityToModel(ds)
	m.ID = uint(ds.Id)
	if err := wdb.Omit("created_at").Save(m).Error; err != nil {
		return bizerr.WrapGormError(err, "update data synchronization failed")
	}

	// Re-load timestamps from DB to keep CreateTime/UpdateTime accurate.
	var updated schema.DataSynchronization
	if err := wdb.Where("id = ?", ds.Id).First(&updated).Error; err == nil {
		ds.CreateTime = toProtoTime(updated.CreatedAt)
		ds.UpdateTime = toProtoTime(updated.UpdatedAt)
	}
	return nil
}

func (r *DataSynchronizationRepoImpl) Delete(ctx context.Context, id uint64) error {
	ctx, span := tracer.Start(ctx, "DataSynchronizationRepo.Delete")
	defer span.End()

	if id == 0 {
		return bizerr.New(bizerr.ErrCodeParams, "id is required")
	}

	wdb := r.dbProvider.WriteDB(ctx)
	if err := wdb.Where("id = ?", id).Delete(&schema.DataSynchronization{}).Error; err != nil {
		return bizerr.WrapGormError(err, "delete data synchronization failed")
	}
	return nil
}

func cvtDataSynchronizationEntityToModel(ds *entity.DataSynchronization) *schema.DataSynchronization {
	// Persist enum state as string in DB for readability.
	status := ds.GetState().String()
	if ds.GetState() == tidbmgmtservicev1.DataSynchronization_STATE_UNSPECIFIED || status == "" {
		status = tidbmgmtservicev1.DataSynchronization_PENDING.String()
	}
	rcu := ds.GetRcu()
	if rcu == 0 {
		rcu = 256
	}

	var startTso *string
	if tso := ds.GetStartTso(); tso != "" {
		startTso = &tso
	}

	out := &schema.DataSynchronization{
		SourceClusterID:       ds.GetSourceClusterId(),
		TargetClusterID:       ds.GetTargetClusterId(),
		TargetTidbGroupID:     ds.GetTargetTidbGroupId(),
		Name:                  ds.GetName(),
		Description:           ds.GetDescription(),
		Status:                status,
		Rcu:                   rcu,
		ChangefeedConnectorID: ds.ChangefeedConnectorID,
		SinkEndpointID:        ds.SinkEndpointID,
		AvailableZoneIDs:      schema.StringSlice(ds.AvailableZoneIDs),
		StartTso:              startTso,
	}
	return out
}

func cvtDataSynchronizationModelToEntity(m *schema.DataSynchronization) *entity.DataSynchronization {
	state := tidbmgmtservicev1.DataSynchronization_STATE_UNSPECIFIED
	if m != nil {
		if v, ok := tidbmgmtservicev1.DataSynchronization_State_value[m.Status]; ok {
			state = tidbmgmtservicev1.DataSynchronization_State(v)
		} else if m.Status == "" {
			state = tidbmgmtservicev1.DataSynchronization_PENDING
		}
	}
	startTso := ""
	if m.StartTso != nil {
		startTso = *m.StartTso
	}

	return &entity.DataSynchronization{
		DataSynchronization: tidbmgmtservicev1.DataSynchronization{
			Id:                uint64(m.ID),
			SourceClusterId:   m.SourceClusterID,
			TargetClusterId:   m.TargetClusterID,
			TargetTidbGroupId: m.TargetTidbGroupID,
			Name:              m.Name,
			Description:       m.Description,
			State:             state,
			Rcu:               m.Rcu,
			StartTso:          startTso,
			CreateTime:        toProtoTime(m.CreatedAt),
			UpdateTime:        toProtoTime(m.UpdatedAt),
		},
		ChangefeedConnectorID: m.ChangefeedConnectorID,
		SinkEndpointID:        m.SinkEndpointID,
		AvailableZoneIDs:      []string(m.AvailableZoneIDs),
	}
}

func toProtoTime(t time.Time) *timestamppb.Timestamp {
	if t.IsZero() {
		return nil
	}
	return timestamppb.New(t)
}
