package server

import (
	"context"
	"fmt"
	"net/url"
	"os"
	"sync"

	"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	osscredentials "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
	credproviders "github.com/aliyun/credentials-go/credentials/providers"
	"github.com/pingcap/errors"
)

var _ LogHandler = &OSSHandler{}

// OSSHandler handle pushing log to AliCloud OSS bucket.
type OSSHandler struct {
	Config
	bucket string
	client *oss.Client
}

//nolint:gocritic //config will be reloaded
func NewOSSHandler(config Config) (*OSSHandler, error) {
	h := &OSSHandler{
		Config: config,
	}

	u, err := url.Parse(config.BucketURL)
	if err != nil {
		return nil, errors.Annotatef(err, "invalid url %q", config.BucketURL)
	}

	h.bucket = u.Host

	if err := h.buildOSSClient(); err != nil {
		return nil, errors.Annotate(err, "failed to build oss client")
	}

	return h, nil
}

func (h *OSSHandler) buildOSSClient() error {
	// Determine credentials provider
	var provider osscredentials.CredentialsProvider
	if h.AliCloud == nil || h.AliCloud.RoleARN == "" {
		// Use node role
		provider = osscredentials.NewEcsRoleCredentialsProvider()
	} else {
		// Use assume role (ECS RAM Role -> RAM Role ARN)
		p, err := newAliCloudAssumeRoleCredentialsProvider(h.AliCloud)
		if err != nil {
			return err
		}
		provider = p
	}

	// Determine region: prefer configured, otherwise infer from endpoint
	region := h.Region
	if region == "" {
		return fmt.Errorf("region not set")
	}

	cfg := oss.LoadDefaultConfig().
		WithCredentialsProvider(provider).
		WithRegion(region).
		WithUseInternalEndpoint(true)

	h.client = oss.NewClient(cfg)
	return nil
}

type threadSafeAliCloudCredentialsProvider struct {
	mu       sync.Mutex
	provider credproviders.CredentialsProvider
}

func (p *threadSafeAliCloudCredentialsProvider) GetCredentials() (*credproviders.Credentials, error) {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.provider.GetCredentials()
}

func (p *threadSafeAliCloudCredentialsProvider) GetProviderName() string {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.provider.GetProviderName()
}

func newAliCloudOSSCredentialsProvider(provider credproviders.CredentialsProvider) osscredentials.CredentialsProvider {
	return osscredentials.CredentialsProviderFunc(func(ctx context.Context) (osscredentials.Credentials, error) {
		if err := ctx.Err(); err != nil {
			return osscredentials.Credentials{}, err
		}

		cred, err := provider.GetCredentials()
		if err != nil {
			return osscredentials.Credentials{}, err
		}
		if cred == nil {
			return osscredentials.Credentials{}, fmt.Errorf("credential provider returned nil credentials")
		}

		return osscredentials.Credentials{
			AccessKeyID:     cred.AccessKeyId,
			AccessKeySecret: cred.AccessKeySecret,
			SecurityToken:   cred.SecurityToken,
		}, nil
	})
}

func newAliCloudAssumeRoleCredentialsProvider(aliCfg *AliCloud) (osscredentials.CredentialsProvider, error) {
	if aliCfg == nil || aliCfg.RoleARN == "" {
		return nil, fmt.Errorf("alicloud.roleARN not set")
	}

	baseProvider, err := credproviders.NewECSRAMRoleCredentialsProviderBuilder().Build()
	if err != nil {
		return nil, fmt.Errorf("failed to build ecs ram role credentials provider: %w", err)
	}

	builder := credproviders.NewRAMRoleARNCredentialsProviderBuilder().
		WithCredentialsProvider(baseProvider).
		WithRoleArn(aliCfg.RoleARN).
		WithRoleSessionName(aliCfg.RoleSessionName)
	if aliCfg.ExternalID != "" {
		builder.WithExternalId(aliCfg.ExternalID)
	}

	roleProvider, err := builder.Build()
	if err != nil {
		return nil, fmt.Errorf("failed to build ram role arn credentials provider: %w", err)
	}

	return newAliCloudOSSCredentialsProvider(&threadSafeAliCloudCredentialsProvider{
		provider: roleProvider,
	}), nil
}

// Push implements LogHandler interface.
func (h *OSSHandler) Push(ctx context.Context, fname, targetKey string) (err error) {
	file, err := os.Open(fname)
	if err != nil {
		return errors.Annotatef(err, "failed to open file %q", fname)
	}
	defer file.Close()

	// Upload file to AliCloud
	_, err = h.client.PutObject(ctx, &oss.PutObjectRequest{
		Bucket: oss.Ptr(h.bucket),
		Key:    oss.Ptr(targetKey),
		Body:   file,
	})
	if err != nil {
		return errors.Annotate(err, "failed to upload to AliCloud")
	}

	return nil
}

func (h *OSSHandler) Close() {}
