// Package storage 封装腾讯云 COS 对象存储访问，统一提供上传、读取、删除和公开地址生成能力。
package storage

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"strings"

	"juhe-factory/api/internal/config"

	"github.com/aws/aws-sdk-go-v2/aws"
	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

const publicMediaCacheControl = "public, max-age=31536000, immutable"

// COS 保存腾讯云 COS 客户端、存储桶、公开域名和媒体体积限制。
type COS struct {
	client        *s3.Client
	bucket        string
	publicBaseURL string
	maxImageBytes int64
	maxVideoBytes int64
	maxAudioBytes int64
}

// NewCOS 根据服务端配置初始化腾讯云 COS；完全未配置时返回 nil，部分配置时返回错误。
func NewCOS(ctx context.Context, cfg config.Config) (*COS, error) {
	configured := cfg.COSEndpoint != "" || cfg.COSSecretID != "" || cfg.COSSecretKey != "" || cfg.COSBucket != "" || cfg.COSRegion != ""
	if !configured {
		return nil, nil
	}
	if cfg.COSEndpoint == "" || cfg.COSSecretID == "" || cfg.COSSecretKey == "" || cfg.COSBucket == "" || cfg.COSRegion == "" {
		return nil, fmt.Errorf("COS 配置不完整，必须同时配置 Region、Endpoint、SecretId、SecretKey 和 Bucket")
	}
	if cfg.COSPublicBaseURL == "" {
		return nil, fmt.Errorf("视频转绘需要可供模型读取的 COS_PUBLIC_BASE_URL")
	}
	endpoint := normalizeHTTPURL(cfg.COSEndpoint)
	publicBaseURL := normalizeHTTPURL(cfg.COSPublicBaseURL)
	awsCfg, err := awsconfig.LoadDefaultConfig(ctx,
		awsconfig.WithRegion(cfg.COSRegion),
		awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(cfg.COSSecretID, cfg.COSSecretKey, "")),
	)
	if err != nil {
		return nil, err
	}
	client := s3.NewFromConfig(awsCfg, func(options *s3.Options) {
		options.BaseEndpoint = aws.String(endpoint)
		options.UsePathStyle = false
	})
	return &COS{
		client: client, bucket: cfg.COSBucket, publicBaseURL: publicBaseURL,
		maxImageBytes: int64(cfg.COSMaxImageSizeMB) * 1024 * 1024,
		maxVideoBytes: int64(cfg.COSMaxVideoSizeMB) * 1024 * 1024,
		maxAudioBytes: int64(cfg.COSMaxAudioSizeMB) * 1024 * 1024,
	}, nil
}

// normalizeHTTPURL 补全缺少协议的 HTTP 地址，并保留显式配置的协议。
func normalizeHTTPURL(value string) string {
	value = strings.TrimSpace(value)
	if value != "" && !strings.Contains(value, "://") {
		return "https://" + value
	}
	return value
}

// MaxVideoBytes 返回允许处理的视频最大字节数。
func (storage *COS) MaxVideoBytes() int64 {
	if storage == nil {
		return 0
	}
	return storage.maxVideoBytes
}

// MaxAudioBytes 返回允许处理的音频最大字节数。
func (storage *COS) MaxAudioBytes() int64 {
	if storage == nil {
		return 0
	}
	return storage.maxAudioBytes
}

// PublicURL 根据对象键生成无需签名的公开访问地址。
func (storage *COS) PublicURL(key string) string {
	if storage == nil || storage.publicBaseURL == "" {
		return ""
	}
	return strings.TrimRight(storage.publicBaseURL, "/") + "/" + strings.TrimLeft(key, "/")
}

// MaxImageBytes 返回允许处理的图片最大字节数。
func (storage *COS) MaxImageBytes() int64 {
	if storage == nil {
		return 0
	}
	return storage.maxImageBytes
}

// Put 上传对象并返回其公开访问地址。
func (storage *COS) Put(ctx context.Context, key, contentType string, body io.Reader, size int64) (string, error) {
	if storage == nil {
		return "", fmt.Errorf("COS 未配置")
	}
	input := &s3.PutObjectInput{
		Bucket:       &storage.bucket,
		Key:          &key,
		Body:         body,
		ContentType:  &contentType,
		CacheControl: aws.String(publicMediaCacheControl),
	}
	if size >= 0 {
		input.ContentLength = aws.Int64(size)
	}
	if _, err := storage.client.PutObject(ctx, input); err != nil {
		return "", err
	}
	return storage.PublicURL(key), nil
}

// Open 打开对象读取流，并返回内容类型和内容长度；调用方负责关闭读取流。
func (storage *COS) Open(ctx context.Context, key string) (io.ReadCloser, string, int64, error) {
	if storage == nil || strings.TrimSpace(key) == "" {
		return nil, "", 0, fmt.Errorf("COS 未配置或对象不存在")
	}
	result, err := storage.client.GetObject(ctx, &s3.GetObjectInput{Bucket: &storage.bucket, Key: &key})
	if err != nil {
		return nil, "", 0, err
	}
	return result.Body, aws.ToString(result.ContentType), aws.ToInt64(result.ContentLength), nil
}

// Delete 删除指定对象；空对象键视为无需处理。
func (storage *COS) Delete(ctx context.Context, key string) error {
	if storage == nil || strings.TrimSpace(key) == "" {
		return nil
	}
	_, err := storage.client.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket: &storage.bucket, Key: &key})
	return err
}

// PutFromURL 下载远端结果到临时文件，校验体积后上传到 COS。
func (storage *COS) PutFromURL(ctx context.Context, client *http.Client, sourceURL, key, fallbackContentType string, maxBytes int64) (string, string, string, int64, error) {
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
	if err != nil {
		return "", "", "", 0, err
	}
	response, err := client.Do(request)
	if err != nil {
		return "", "", "", 0, err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return "", "", "", 0, fmt.Errorf("下载生成结果失败: HTTP %d", response.StatusCode)
	}
	if maxBytes > 0 && response.ContentLength > maxBytes {
		return "", "", "", 0, fmt.Errorf("生成结果超过允许大小")
	}
	contentType := strings.TrimSpace(strings.Split(response.Header.Get("Content-Type"), ";")[0])
	if contentType == "" || contentType == "application/octet-stream" {
		contentType = fallbackContentType
	}
	key = keyWithContentType(key, contentType)
	temporary, err := os.CreateTemp("", "jcf-generated-*")
	if err != nil {
		return "", "", "", 0, err
	}
	temporaryName := temporary.Name()
	defer os.Remove(temporaryName)
	reader := io.Reader(response.Body)
	if maxBytes > 0 {
		reader = io.LimitReader(response.Body, maxBytes+1)
	}
	written, copyErr := io.Copy(temporary, reader)
	if closeErr := temporary.Close(); copyErr == nil {
		copyErr = closeErr
	}
	if copyErr != nil {
		return "", "", "", 0, copyErr
	}
	if maxBytes > 0 && written > maxBytes {
		return "", "", "", 0, fmt.Errorf("生成结果超过允许大小")
	}
	file, err := os.Open(temporaryName)
	if err != nil {
		return "", "", "", 0, err
	}
	defer file.Close()
	url, err := storage.Put(ctx, key, contentType, file, written)
	if err != nil {
		return "", "", "", 0, err
	}
	return url, key, contentType, written, nil
}

// keyWithContentType 根据真实媒体类型修正对象扩展名。
func keyWithContentType(key, contentType string) string {
	extension := map[string]string{
		"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp", "image/gif": ".gif",
		"video/mp4": ".mp4", "video/webm": ".webm", "video/quicktime": ".mov",
	}[strings.ToLower(contentType)]
	if extension == "" {
		return key
	}
	return strings.TrimSuffix(key, path.Ext(key)) + extension
}
