package s3store

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

	"apps/backend/internal/module/filestorage/domain"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-sdk-go-v2/service/s3/types"
	"github.com/zeromicro/go-zero/core/logx"
)

// Config for MinIO / S3-compatible storage.
type Config struct {
	Endpoint      string // e.g. http://127.0.0.1:9000
	Region        string
	Bucket        string
	AccessKey     string
	SecretKey     string
	UsePathStyle  bool
	PublicBaseURL string // e.g. http://127.0.0.1:9000/haixun-assets  (no trailing slash)
}

type Store struct {
	client *s3.Client
	cfg    Config
}

func New(cfg Config) (*Store, error) {
	if cfg.Endpoint == "" || cfg.Bucket == "" {
		return nil, fmt.Errorf("s3store: empty endpoint or bucket")
	}
	if cfg.Region == "" {
		cfg.Region = "us-east-1"
	}
	cfg.Endpoint = strings.TrimRight(cfg.Endpoint, "/")
	cfg.PublicBaseURL = strings.TrimRight(cfg.PublicBaseURL, "/")

	client := s3.New(s3.Options{
		Region: cfg.Region,
		Credentials: credentials.NewStaticCredentialsProvider(
			cfg.AccessKey, cfg.SecretKey, "",
		),
		BaseEndpoint: aws.String(cfg.Endpoint),
		UsePathStyle: cfg.UsePathStyle,
	})

	s := &Store{client: client, cfg: cfg}
	if err := s.ensureBucket(context.Background()); err != nil {
		logx.Errorf("s3store ensure bucket %s: %v (will retry on upload)", cfg.Bucket, err)
	}
	// Meta Threads 發圖會從公網 cURL image_url；bucket 必須允許匿名 GetObject
	if err := s.ensurePublicRead(context.Background()); err != nil {
		logx.Errorf("s3store public-read policy %s: %v (Meta may fail to fetch images)", cfg.Bucket, err)
	} else {
		logx.Infof("s3store bucket %s public GetObject enabled (for Threads image_url)", cfg.Bucket)
	}
	return s, nil
}

func (s *Store) Name() string    { return "s3" }
func (s *Store) Enabled() bool   { return true }

func (s *Store) ensureBucket(ctx context.Context) error {
	_, err := s.client.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: aws.String(s.cfg.Bucket)})
	if err == nil {
		return nil
	}
	_, err = s.client.CreateBucket(ctx, &s3.CreateBucketInput{
		Bucket: aws.String(s.cfg.Bucket),
	})
	if err != nil {
		// race: already exists
		var exists *types.BucketAlreadyOwnedByYou
		var exists2 *types.BucketAlreadyExists
		if strings.Contains(err.Error(), "BucketAlready") ||
			strings.Contains(err.Error(), "already own") {
			return nil
		}
		_ = exists
		_ = exists2
		return err
	}
	return nil
}

// ensurePublicRead — 允許匿名讀物件（nginx /haixun-assets → MinIO 才能給 Meta 抓圖）
func (s *Store) ensurePublicRead(ctx context.Context) error {
	// MinIO / S3 compatible anonymous GetObject
	policy := fmt.Sprintf(`{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": {"AWS": ["*"]},
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::%s/*"]
    }
  ]
}`, s.cfg.Bucket)
	_, err := s.client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
		Bucket: aws.String(s.cfg.Bucket),
		Policy: aws.String(policy),
	})
	return err
}

func (s *Store) Upload(ctx context.Context, key string, data []byte, contentType string) (string, error) {
	key = strings.TrimPrefix(key, "/")
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	_, err := s.client.PutObject(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(s.cfg.Bucket),
		Key:         aws.String(key),
		Body:        bytes.NewReader(data),
		ContentType: aws.String(contentType),
		// MinIO: ACL may be disabled; skip ACL for compatibility
	})
	if err != nil {
		return "", fmt.Errorf("s3 put: %w", err)
	}
	return s.publicURL(key), nil
}

func (s *Store) Delete(ctx context.Context, key string) error {
	key = strings.TrimPrefix(key, "/")
	_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(s.cfg.Bucket),
		Key:    aws.String(key),
	})
	return err
}

func (s *Store) publicURL(key string) string {
	key = strings.TrimPrefix(key, "/")
	if s.cfg.PublicBaseURL != "" {
		return s.cfg.PublicBaseURL + "/" + key
	}
	// path-style: http://endpoint/bucket/key
	return s.cfg.Endpoint + "/" + s.cfg.Bucket + "/" + escapePath(key)
}

func escapePath(key string) string {
	parts := strings.Split(key, "/")
	for i, p := range parts {
		parts[i] = url.PathEscape(p)
	}
	return strings.Join(parts, "/")
}

var _ domain.Storage = (*Store)(nil)
