package export

import (
	"context"
	"errors"

	"github.com/tidbcloud/pkgv2/db"
	"go.opentelemetry.io/otel"
	"gorm.io/gorm"

	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/remoteencryptor"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/export"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/common"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/pagination"
)

var tracer = otel.Tracer("export_repo_impl")

type exportRepoImpl struct {
	provider  db.Provider
	encryptor remoteencryptor.Client
}

// NewExportRepo returns an implementation of export.ExportRepo.
func NewExportRepo(provider db.Provider, encryptor remoteencryptor.Client) export.ExportRepo {
	return &exportRepoImpl{provider: provider, encryptor: encryptor}
}

func (r *exportRepoImpl) Create(ctx context.Context, e *entity.Export) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "exportRepo.Create")
	defer span.End()

	wdb := r.provider.WriteDB(ctx)
	schemaExport, err := r.encryptExport(ctx, e.Export)
	if err != nil {
		return err
	}

	if err := wdb.Create(schemaExport).Error; err != nil {
		return bizerr.WrapGormError(err, "failed to create export")
	}
	e.Model = schemaExport.Model

	return nil
}

func (r *exportRepoImpl) Save(ctx context.Context, e *entity.Export) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "exportRepo.Save")
	defer span.End()

	wdb := r.provider.WriteDB(ctx)
	schemaExport, err := r.encryptExport(ctx, e.Export)
	if err != nil {
		return err
	}

	return common.SaveWithVersioning(wdb, schemaExport, nil)
}

func (r *exportRepoImpl) GetByClusterAndExportId(ctx context.Context, clusterId, exportId string) (*entity.Export, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "exportRepo.GetByClusterAndExportId")
	defer span.End()

	rdb := r.provider.ReadDB(ctx)
	var s schema.Export
	if err := rdb.Where("cluster_id = ? AND export_id = ?", clusterId, exportId).First(&s).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerr.New(bizerr.ErrCodeRecordNotFound, "export not found")
		}
		return nil, bizerr.WrapGormError(err, "failed to get export")
	}

	decryptedExport, err := r.decryptExport(ctx, &s)
	if err != nil {
		return nil, err
	}

	return &entity.Export{Export: decryptedExport}, nil
}

func (r *exportRepoImpl) Count(ctx context.Context, filter *export.QueryExportsFilter) (int64, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "exportRepo.Count")
	defer span.End()

	rdb := r.provider.ReadDB(ctx)
	query := rdb.Model(&schema.Export{})

	if len(filter.ClusterIds) > 0 {
		query = query.Where("cluster_id IN ?", filter.ClusterIds)
	}
	if len(filter.States) > 0 {
		query = query.Where("state IN ?", filter.States)
	}

	var total int64
	if err := query.Count(&total).Error; err != nil {
		return 0, bizerr.WrapGormError(err, "failed to count exports")
	}
	return total, nil
}

func (r *exportRepoImpl) Delete(ctx context.Context, clusterId, exportId string) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "exportRepo.Delete")
	defer span.End()

	wdb := r.provider.WriteDB(ctx)
	result := wdb.Where("cluster_id = ? AND export_id = ? AND state != ?", clusterId, exportId, schema.ExportStateRunning).Delete(&schema.Export{})
	if result.Error != nil {
		return bizerr.WrapGormError(result.Error, "failed to delete export")
	}
	if result.RowsAffected == 0 {
		return bizerr.New(bizerr.ErrCodeRecordNotFound, "export is in running state, please cancel it first")
	}

	return nil
}

func (r *exportRepoImpl) Cancel(ctx context.Context, clusterId, exportId string) bizerr.BizError {
	ctx, span := tracer.Start(ctx, "exportRepo.Cancel")
	defer span.End()

	wdb := r.provider.WriteDB(ctx)
	result := wdb.Model(&schema.Export{}).Where("cluster_id = ? AND export_id = ? AND state = ?", clusterId, exportId, schema.ExportStateRunning).
		Update("state", schema.ExportStateCanceled)
	if result.Error != nil {
		return bizerr.WrapGormError(result.Error, "failed to cancel export")
	}
	if result.RowsAffected == 0 {
		return bizerr.New(bizerr.ErrCodeRecordNotFound, "export can not be canceled because it is not in running state")
	}
	return nil
}

func (r *exportRepoImpl) List(ctx context.Context, filter *export.QueryExportsFilter, page *pagination.Page) ([]*entity.Export, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "exportRepo.List")
	defer span.End()

	rdb := r.provider.ReadDB(ctx)
	query := rdb.Model(&schema.Export{})

	if len(filter.ClusterIds) > 0 {
		query = query.Where("cluster_id IN ?", filter.ClusterIds)
	}
	if len(filter.States) > 0 {
		query = query.Where("state IN ?", filter.States)
	}

	if page != nil {
		page = pagination.NormalizePage(page)
		query = query.Limit(int(page.PageSize))
		query = query.Offset(int(page.Offset))
	}

	query = query.Order("created_at DESC")

	var list []*schema.Export
	if err := query.Find(&list).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "failed to list exports")
	}

	out := make([]*entity.Export, 0, len(list))
	for _, e := range list {
		decryptedExport, err := r.decryptExport(ctx, e)
		if err != nil {
			return nil, err
		}
		out = append(out, &entity.Export{Export: decryptedExport})
	}
	return out, nil
}
