package syncer

import (
	"context"
	"fmt"

	"github.com/sirupsen/logrus"
	"go.uber.org/multierr"

	azblobcli "github.com/tidbcloud/observability/backup-worker/pkg/client/azblob"
	gcscli "github.com/tidbcloud/observability/backup-worker/pkg/client/gcs"
	osscli "github.com/tidbcloud/observability/backup-worker/pkg/client/oss"
	s3cli "github.com/tidbcloud/observability/backup-worker/pkg/client/s3"
	"github.com/tidbcloud/observability/backup-worker/pkg/common"
	"github.com/tidbcloud/observability/backup-worker/pkg/configs"
)

// Syncer defines the contract for cross-bucket object replication.
//
// Each implementation wraps a pair of storage clients (source and destination)
// and uses provider-specific server-side copy APIs to transfer objects without
// routing data through the worker process.
type Syncer interface {
	// Type returns the backend provider identifier for logging.
	Type() string

	// Sync lists all objects under the shard's source prefix and copies each
	// to the destination. Returns the total bytes transferred.
	Sync(ctx context.Context, shard configs.FileShard) (int64, error)

	// Close releases resources held by both source and destination clients.
	Close() error
}

func New(ctx context.Context, cfg configs.Config) (Syncer, error) {
	if cfg.Syncers.AzBlob.Enabled {
		return newAzBlob(ctx, cfg.Syncers.AzBlob, cfg.RetryPolicy)
	}
	if cfg.Syncers.Gcs.Enabled {
		return newGCS(ctx, cfg.Syncers.Gcs, cfg.RetryPolicy)
	}
	if cfg.Syncers.Oss.Enabled {
		return newOSS(ctx, cfg.Syncers.Oss, cfg.RetryPolicy)
	}
	if cfg.Syncers.S3.Enabled {
		return newS3(ctx, cfg.Syncers.S3, cfg.RetryPolicy)
	}
	return nil, fmt.Errorf("no syncer enabled")
}

func (s *baseSyncer) Close() error {
	return multierr.Combine(s.srcCli.Close(), s.destCli.Close())
}

func newAzBlob(ctx context.Context, cfg configs.AzBlobSyncer, rp configs.RetryPolicy) (*baseSyncer, error) {
	srcCli, err := azblobcli.New(ctx, cfg.Source, rp)
	if err != nil {
		return nil, fmt.Errorf("create source azblob client: %w", err)
	}
	destCli, err := azblobcli.New(ctx, cfg.Dest, rp)
	if err != nil {
		srcCli.Close()
		return nil, fmt.Errorf("create dest azblob client: %w", err)
	}
	logrus.Infof("azblob syncer created: src=%s/%s, dest=%s/%s",
		cfg.Source.AccountName, cfg.Source.Container,
		cfg.Dest.AccountName, cfg.Dest.Container,
	)
	return &baseSyncer{
		provider:       common.StorageAZBlob,
		srcBucket:      cfg.Source.Container,
		srcCli:         srcCli,
		destCli:        destCli,
		destPathPrefix: cfg.Dest.PathPrefix,
		copyFunc: func(ctx context.Context, srcBucket, srcKey, destKey string, size int64) error {
			return destCli.CopyObject(ctx, srcBucket, srcKey, destKey, srcCli)
		},
	}, nil
}

func newGCS(ctx context.Context, cfg configs.GcsSyncer, rp configs.RetryPolicy) (*baseSyncer, error) {
	srcCli, err := gcscli.New(ctx, cfg.Source, rp)
	if err != nil {
		return nil, fmt.Errorf("create source gcs client: %w", err)
	}
	destCli, err := gcscli.New(ctx, cfg.Dest, rp)
	if err != nil {
		srcCli.Close()
		return nil, fmt.Errorf("create dest gcs client: %w", err)
	}
	logrus.Infof("gcs syncer created: src=%s/%s, dest=%s/%s",
		cfg.Source.Bucket, cfg.Source.PathPrefix,
		cfg.Dest.Bucket, cfg.Dest.PathPrefix,
	)
	return &baseSyncer{
		provider:       common.StorageGCS,
		srcBucket:      cfg.Source.Bucket,
		srcCli:         srcCli,
		destCli:        destCli,
		destPathPrefix: cfg.Dest.PathPrefix,
		copyFunc: func(ctx context.Context, srcBucket, srcKey, destKey string, size int64) error {
			return destCli.CopyObject(ctx, srcBucket, srcKey, destKey)
		},
	}, nil
}

func newOSS(ctx context.Context, cfg configs.OssSyncer, rp configs.RetryPolicy) (*baseSyncer, error) {
	srcCli, err := osscli.New(ctx, cfg.Source, rp)
	if err != nil {
		return nil, fmt.Errorf("create source oss client: %w", err)
	}
	destCli, err := osscli.New(ctx, cfg.Dest, rp)
	if err != nil {
		srcCli.Close()
		return nil, fmt.Errorf("create dest oss client: %w", err)
	}
	logrus.Infof("oss syncer created: src=%s/%s, dest=%s/%s",
		cfg.Source.Endpoint, cfg.Source.Bucket,
		cfg.Dest.Endpoint, cfg.Dest.Bucket,
	)
	return &baseSyncer{
		provider:       common.StorageOSS,
		srcBucket:      cfg.Source.Bucket,
		srcCli:         srcCli,
		destCli:        destCli,
		destPathPrefix: cfg.Dest.PathPrefix,
		copyFunc: func(ctx context.Context, srcBucket, srcKey, destKey string, size int64) error {
			return destCli.CopyObject(ctx, srcBucket, srcKey, destKey, size)
		},
	}, nil
}

func newS3(ctx context.Context, cfg configs.S3Syncer, rp configs.RetryPolicy) (*baseSyncer, error) {
	srcCli, err := s3cli.New(ctx, cfg.Source, rp)
	if err != nil {
		return nil, fmt.Errorf("create source s3 client: %w", err)
	}
	destCli, err := s3cli.New(ctx, cfg.Dest, rp)
	if err != nil {
		srcCli.Close()
		return nil, fmt.Errorf("create dest s3 client: %w", err)
	}
	logrus.Infof("s3 syncer created: src=%s/%s, dest=%s/%s",
		cfg.Source.Region, cfg.Source.Bucket,
		cfg.Dest.Region, cfg.Dest.Bucket,
	)
	return &baseSyncer{
		provider:       common.StorageS3,
		srcBucket:      cfg.Source.Bucket,
		srcCli:         srcCli,
		destCli:        destCli,
		destPathPrefix: cfg.Dest.PathPrefix,
		copyFunc: func(ctx context.Context, srcBucket, srcKey, destKey string, size int64) error {
			return destCli.CopyObject(ctx, srcBucket, srcKey, destKey, size)
		},
	}, nil
}
