package alioss

import (
	"context"
	"fmt"
	"net/url"
	"strings"

	"github.com/aliyun/aliyun-oss-go-sdk/oss"
	"github.com/pingcap/errors" // Keeping the original error package for compatibility
)

// OSSConfig holds all necessary parameters for creating an OSS client.
type OSSConfig struct {
	AccessKeyID     string
	SecretAccessKey string
	Region          string // Expected OSS Region ID, e.g., "cn-hangzhou".
	Endpoint        string // Full OSS Endpoint URL. If set, it overrides the generated one.
	UseInternal     bool   // Whether to use the internal (VPC) endpoint.
}

// OSSOption defines the signature for functional options to configure OSSConfig.
type OSSOption func(*OSSConfig)

// WithAccessKey sets the AccessKeyID and SecretAccessKey.
// 设置 AccessKeyID 和 SecretAccessKey。
func WithAccessKey(id, secret string) OSSOption {
	return func(cfg *OSSConfig) {
		cfg.AccessKeyID = id
		cfg.SecretAccessKey = secret
	}
}

// WithRegion sets the OSS Region ID. The endpoint will be generated from this if not explicitly set.
func WithRegion(region string) OSSOption {
	return func(cfg *OSSConfig) {
		cfg.Region = region
	}
}

// WithEndpoint sets the full OSS Endpoint URL, which overrides the region-based generation.
func WithEndpoint(endpoint string) OSSOption {
	return func(cfg *OSSConfig) {
		cfg.Endpoint = endpoint
	}
}

// WithInternalEndpoint marks whether to use the internal (VPC) endpoint format.
func WithInternalEndpoint(useInternal bool) OSSOption {
	return func(cfg *OSSConfig) {
		cfg.UseInternal = useInternal
	}
}

// -----------------------------------------------------------------------------
// Endpoint Generation
// -----------------------------------------------------------------------------

const (
	// PublicHostFormat is the format for the public internet endpoint (e.g., https://oss-cn-hangzhou.aliyuncs.com).
	PublicHostFormat = "https://oss-%s.aliyuncs.com"
	// InternalHostFormat is the format for the internal (VPC) endpoint.
	InternalHostFormat = "https://oss-%s-internal.aliyuncs.com"
)

// GenerateOssEndpoint generates the public or internal Endpoint URL based on the region and a flag.
// GenerateOssEndpoint 根据 Region 和标志生成公网或内网 Endpoint URL。
func GenerateOssEndpoint(ossRegion string, useInternal bool) string {
	if useInternal {
		return fmt.Sprintf(InternalHostFormat, ossRegion)
	}
	return fmt.Sprintf(PublicHostFormat, ossRegion)
}

// -----------------------------------------------------------------------------
// Client Creation
// -----------------------------------------------------------------------------

// newClient internally processes the config and creates the *oss.Client instance.
func newClient(cfg *OSSConfig) (*oss.Client, error) {
	if cfg.AccessKeyID == "" || cfg.SecretAccessKey == "" {
		return nil, errors.New("AccessKeyID and SecretAccessKey are required")
	}

	endpoint := cfg.Endpoint
	if endpoint == "" {
		if cfg.Region == "" {
			return nil, errors.New("must provide either Region or full Endpoint")
		}
		// Auto-generate endpoint from region
		endpoint = GenerateOssEndpoint(cfg.Region, cfg.UseInternal)
	}

	// Create an OSS client instance
	client, err := oss.New(endpoint, cfg.AccessKeyID, cfg.SecretAccessKey)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to create OSS client with endpoint %s", endpoint)
	}

	return client, nil
}

// NewOSSClient creates a new OSS client instance using functional options.
func NewOSSClient(opts ...OSSOption) (*oss.Client, error) {
	cfg := &OSSConfig{}
	for _, opt := range opts {
		opt(cfg)
	}

	return newClient(cfg)
}

func ParseOssUrl(ossUrl string) (bucket, pathPrefix string, err error) {
	u, err := url.Parse(ossUrl)
	if err != nil {
		return "", "", errors.Errorf("invalid OSS URL format: %s", ossUrl)
	}
	if u.Scheme != "oss" {
		return "", "", errors.Errorf("invalid scheme: expected 'oss', got '%s'", u.Scheme)
	}

	bucket = u.Host
	// Path contains the prefix, strip leading slash
	pathPrefix = strings.TrimLeft(u.Path, "/")

	if bucket == "" {
		return "", "", errors.New("OSS URL must contain a bucket name (e.g., oss://my-bucket/path)")
	}

	return bucket, pathPrefix, nil
}

// -----------------------------------------------------------------------------
// Detect Region (GetOSSBucketRegion)
// -----------------------------------------------------------------------------

// GetOSSBucketRegion retrieves the region ID (Location) for a given OSS bucket.
// It tries to detect the region even if the initial endpoint is not the bucket's home region,
// relying on the OSS SDK's ability to follow the 301 redirection location.
// This requires the 'oss:GetBucketInfo' permission.
func GetOSSBucketRegion(ctx context.Context, bucketName string, opts ...OSSOption) (string, error) {
	cfg := &OSSConfig{}
	for _, opt := range opts {
		opt(cfg)
	}

	if cfg.AccessKeyID == "" || cfg.SecretAccessKey == "" {
		return "", errors.New("AccessKeyID and SecretAccessKey are required for region detection")
	}

	client, err := newClient(cfg)
	if err != nil {
		return "", errors.Wrap(err, "failed to create client for region detection")
	}

	// Test for oss:GetBucketInfo permission and get location.
	bucketInfo, err := client.GetBucketInfo(bucketName)
	if err != nil {
		// The error context is crucial here to guide the user to check credentials or permissions.
		return "", errors.Annotatef(err, "failed to get bucket info for %s. Check AccessKey/SecretKey or permissions (oss:GetBucketInfo)", bucketName)
	}

	// Extract the Region/Location information
	return bucketInfo.BucketInfo.Location, nil
}

// -----------------------------------------------------------------------------
// Check Connectivity and Permissions (CheckOSSAccess)
// -----------------------------------------------------------------------------

// CheckOSSAccess tests the list and get object permissions for an OSS bucket.
// It assumes the client configuration (Region/Endpoint, credentials) is correct.
func CheckOSSAccess(ctx context.Context, bucketName, pathPrefix string, opts ...OSSOption) error {
	// 1. Parse config and create client
	cfg := &OSSConfig{}
	for _, opt := range opts {
		opt(cfg)
	}

	client, err := newClient(cfg)
	if err != nil {
		return errors.Wrap(err, "failed to create OSS client for access check")
	}

	// 2. Get Bucket instance
	bucket, err := client.Bucket(bucketName)
	if err != nil {
		return errors.Annotatef(err, "failed to get OSS Bucket instance for %s. Check bucket name", bucketName)
	}

	// 3. Test oss:ListObjects
	prefix := strings.TrimLeft(pathPrefix, "/")

	// ListObjects with MaxKeys(1) to minimize resource usage.
	lsRes, err := bucket.ListObjects(oss.Prefix(prefix), oss.MaxKeys(1))
	if err != nil {
		return errors.Annotatef(err, "list objects error (oss:ListObjects) in bucket %s with folder %s", bucketName, prefix)
	}

	if len(lsRes.Objects) == 0 {
		return errors.Errorf("no files found under the given OSS location (bucket=%q, folder=%q)", bucketName, prefix)
	}

	// 4. Test oss:GetObject
	firstObjectKey := lsRes.Objects[0].Key

	rc, err := bucket.GetObject(firstObjectKey)
	if err != nil {
		return errors.Annotatef(err, "get object error (oss:GetObject) for key: %s", firstObjectKey)
	}
	defer rc.Close()

	// All checks passed
	return nil
}
