package objectstorage

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"

	aliyunoss "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
	osscredentials "github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
)

type ossClient interface {
	PutObject(ctx context.Context, request *aliyunoss.PutObjectRequest, optFns ...func(*aliyunoss.Options)) (*aliyunoss.PutObjectResult, error)
	GetObject(ctx context.Context, request *aliyunoss.GetObjectRequest, optFns ...func(*aliyunoss.Options)) (*aliyunoss.GetObjectResult, error)
	DeleteObject(ctx context.Context, request *aliyunoss.DeleteObjectRequest, optFns ...func(*aliyunoss.Options)) (*aliyunoss.DeleteObjectResult, error)
}

type OSSConfig struct {
	Region          string
	Bucket          string
	Prefix          string
	Endpoint        string
	AccessKeyID     string
	AccessKeySecret string
}

type OSSOption func(*ossOptions)

type ossOptions struct {
	httpClient *http.Client
}

func WithOSSHTTPClient(httpClient *http.Client) OSSOption {
	return func(options *ossOptions) {
		options.httpClient = httpClient
	}
}

type ossStorage struct {
	bucket string
	prefix string
	client ossClient
}

// S3Config remains as a compatibility alias for in-flight wiring that still
// uses generic S3 naming while the runtime is already OSS-specific.
type S3Config = OSSConfig

func NewOSS(_ context.Context, cfg OSSConfig, opts ...OSSOption) (Storage, error) {
	trimmedRegion := strings.TrimSpace(cfg.Region)
	if trimmedRegion == "" {
		return nil, fmt.Errorf("media OSS region is required")
	}
	trimmedBucket := strings.TrimSpace(cfg.Bucket)
	if trimmedBucket == "" {
		return nil, fmt.Errorf("media OSS bucket is required")
	}
	trimmedEndpoint := strings.TrimSpace(cfg.Endpoint)
	if trimmedEndpoint == "" {
		return nil, fmt.Errorf("media OSS endpoint is required")
	}
	trimmedKeyID := strings.TrimSpace(cfg.AccessKeyID)
	if trimmedKeyID == "" {
		return nil, fmt.Errorf("media OSS access key id is required")
	}
	trimmedSecret := strings.TrimSpace(cfg.AccessKeySecret)
	if trimmedSecret == "" {
		return nil, fmt.Errorf("media OSS access key secret is required")
	}

	options := ossOptions{}
	for _, opt := range opts {
		if opt != nil {
			opt(&options)
		}
	}

	clientCfg := aliyunoss.LoadDefaultConfig().
		WithRegion(trimmedRegion).
		WithEndpoint(trimmedEndpoint).
		WithCredentialsProvider(osscredentials.NewStaticCredentialsProvider(trimmedKeyID, trimmedSecret))
	if options.httpClient != nil {
		clientCfg = clientCfg.WithHttpClient(options.httpClient)
	}
	client := aliyunoss.NewClient(clientCfg)
	return newOSSStorage(trimmedBucket, cfg.Prefix, client), nil
}

func NewS3(ctx context.Context, cfg S3Config) (Storage, error) {
	return NewOSS(ctx, OSSConfig(cfg))
}

func newOSSStorage(bucket string, prefix string, client ossClient) Storage {
	return &ossStorage{
		bucket: strings.TrimSpace(bucket),
		prefix: strings.Trim(strings.TrimSpace(prefix), "/"),
		client: client,
	}
}

func (storage *ossStorage) Driver() string {
	return DriverOSS
}

func (storage *ossStorage) Put(ctx context.Context, key string, contentType string, body []byte) error {
	objectKey, err := storage.objectKey(key)
	if err != nil {
		return err
	}
	trimmedContentType := strings.TrimSpace(contentType)
	if trimmedContentType == "" {
		trimmedContentType = "application/octet-stream"
	}
	contentLength := int64(len(body))
	_, err = storage.client.PutObject(ctx, &aliyunoss.PutObjectRequest{
		Bucket:        aliyunoss.Ptr(storage.bucket),
		Key:           aliyunoss.Ptr(objectKey),
		ContentType:   aliyunoss.Ptr(trimmedContentType),
		ContentLength: aliyunoss.Ptr(contentLength),
		Body:          bytes.NewReader(body),
	})
	if err != nil {
		return fmt.Errorf("put media object %s: %w", objectKey, err)
	}
	return nil
}

func (storage *ossStorage) Get(ctx context.Context, key string) ([]byte, error) {
	objectKey, err := storage.objectKey(key)
	if err != nil {
		return nil, err
	}
	result, err := storage.client.GetObject(ctx, &aliyunoss.GetObjectRequest{
		Bucket: aliyunoss.Ptr(storage.bucket),
		Key:    aliyunoss.Ptr(objectKey),
	})
	if err != nil {
		return nil, mapOSSReadError(objectKey, err)
	}
	defer result.Body.Close()
	payload, err := io.ReadAll(result.Body)
	if err != nil {
		return nil, fmt.Errorf("read media object %s: %w", objectKey, err)
	}
	return payload, nil
}

func (storage *ossStorage) Delete(ctx context.Context, key string) error {
	objectKey, err := storage.objectKey(key)
	if err != nil {
		return err
	}
	_, err = storage.client.DeleteObject(ctx, &aliyunoss.DeleteObjectRequest{
		Bucket: aliyunoss.Ptr(storage.bucket),
		Key:    aliyunoss.Ptr(objectKey),
	})
	if err != nil {
		return fmt.Errorf("delete media object %s: %w", objectKey, err)
	}
	return nil
}

func (storage *ossStorage) objectKey(key string) (string, error) {
	trimmedKey := strings.Trim(strings.TrimSpace(key), "/")
	if trimmedKey == "" {
		return "", fmt.Errorf("media object key is required")
	}
	if storage.prefix == "" {
		return trimmedKey, nil
	}
	return storage.prefix + "/" + trimmedKey, nil
}

func mapOSSReadError(key string, err error) error {
	var serviceErr *aliyunoss.ServiceError
	if errors.As(err, &serviceErr) {
		if serviceErr.StatusCode == 404 || strings.EqualFold(strings.TrimSpace(serviceErr.Code), "NoSuchKey") {
			return fmt.Errorf("media object %s not found: %w", key, ErrObjectNotFound)
		}
	}
	return fmt.Errorf("get media object %s: %w", key, err)
}
