package utils

import (
	"bytes"
	"fmt"
	"io"
	"net/url"
	"strings"

	"github.com/aliyun/aliyun-oss-go-sdk/oss"
	"github.com/aliyun/credentials-go/credentials"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/juju/errors"
)

func parseOSSPath(ossPath string) (bucket, key string, err error) {
	if !strings.HasPrefix(ossPath, "oss://") {
		return "", "", errors.New("OSS path must start with oss://")
	}

	u, err := url.Parse(ossPath)
	if err != nil {
		return "", "", errors.Annotatef(err, "invalid OSS path: %s", ossPath)
	}

	bucket = u.Host
	key = strings.TrimPrefix(u.Path, "/")
	if bucket == "" || key == "" {
		return "", "", errors.New("invalid OSS path, missing bucket or key")
	}

	return bucket, key, nil
}

func getDefaultCredential() (*credentials.CredentialModel, error) {
	provider, err := credentials.NewCredential(nil)
	if err != nil {
		return nil, errors.Annotatef(err, "create default credential provider")
	}

	cred, err := provider.GetCredential()
	if err != nil {
		return nil, errors.Annotatef(err, "get default credential")
	}

	return cred, nil
}

func getBucketEndpoint(bucket string, cred *credentials.CredentialModel) (string, error) {
	client, err := oss.New("https://oss.aliyuncs.com",
		*cred.AccessKeyId,
		*cred.AccessKeySecret,
		oss.SecurityToken(aws.ToString(cred.SecurityToken)),
	)
	if err != nil {
		return "", errors.Annotatef(err, "create client to get bucket location")
	}

	result, err := client.ListBuckets(oss.Prefix(bucket))
	if err != nil {
		return "", errors.Annotatef(err, "list buckets to get bucket location")
	}

	for _, b := range result.Buckets {
		if b.Name == bucket {
			if b.Location == "" {
				return "", errors.Annotatef(err, "bucket %s has empty location", bucket)
			}
			return fmt.Sprintf("https://%s.aliyuncs.com", b.Location), nil
		}
	}
	return "", errors.Errorf("bucket %s not found", bucket)
}

func newOSSClient(bucket string) (*oss.Bucket, error) {
	cred, err := getDefaultCredential()
	if err != nil {
		return nil, err
	}

	endpoint, err := getBucketEndpoint(bucket, cred)
	if err != nil {
		return nil, err
	}

	client, err := oss.New(endpoint,
		*cred.AccessKeyId,
		*cred.AccessKeySecret,
		oss.SecurityToken(aws.ToString(cred.SecurityToken)),
	)
	if err != nil {
		return nil, errors.Annotatef(err, "create OSS client")
	}

	return client.Bucket(bucket)
}

func ReadOssFile(ossUri string) ([]byte, error) {
	bucketName, key, err := parseOSSPath(ossUri)
	if err != nil {
		return nil, err
	}

	bucket, err := newOSSClient(bucketName)
	if err != nil {
		return nil, err
	}

	reader, err := bucket.GetObject(key)
	if err != nil {
		if ossErr, ok := err.(oss.ServiceError); ok && ossErr.Code == "NoSuchKey" {
			return nil, errors.NotFoundf(ossUri)
		}
		return nil, errors.Annotatef(err, "failed to read oss object: %s", ossUri)
	}
	defer reader.Close()

	bytes, err := io.ReadAll(reader)
	if err != nil {
		return nil, errors.Annotatef(err, "read OSS object %s", ossUri)
	}

	return bytes, nil
}

func WriteOssFile(ossUri string, content []byte) error {
	bucketName, key, err := parseOSSPath(ossUri)
	if err != nil {
		return err
	}

	bucket, err := newOSSClient(bucketName)
	if err != nil {
		return err
	}

	err = bucket.PutObject(key, bytes.NewReader(content))
	if err != nil {
		return errors.Annotatef(err, "write OSS object %s", ossUri)
	}

	return nil
}
