package storage

import (
	"context"
	"fmt"
	"io"
	"path"
	"regexp"
	"strings"
	"time"

	"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"
	"github.com/google/uuid"

	appconfig "security-center-backend/internal/config"
)

type UploadResult struct {
	UploadURL string `json:"upload_url"`
	Key       string `json:"key"`
	ExpiresIn int64  `json:"expires_in"` // seconds
}

type SignedURLResult struct {
	URL       string `json:"url"`
	ExpiresIn int64  `json:"expires_in"` // seconds
}

type ObjectInfo struct {
	ContentType string `json:"content_type"`
	SizeBytes   int64  `json:"size_bytes"`
	ETag        string `json:"etag"`
}

type FileUploader interface {
	PresignPut(ctx context.Context, filename string, contentType string) (*UploadResult, error)
	PresignGet(ctx context.Context, key string) (*SignedURLResult, error)
	ObjectURL(key string) string
	Bucket() string
	Head(ctx context.Context, key string) (*ObjectInfo, error)
	Get(ctx context.Context, key string) (io.ReadCloser, *ObjectInfo, error)
	Delete(ctx context.Context, key string) error
}

type S3Uploader struct {
	client     *s3.Client
	presign    *s3.PresignClient
	bucket     string
	baseURL    string
	keyPrefix  string
	presignTTL time.Duration
}

func NewS3Uploader(ctx context.Context, cfg appconfig.S3Config) (*S3Uploader, error) {
	if !cfg.Enabled {
		return nil, fmt.Errorf("s3 uploader disabled")
	}
	if cfg.Bucket == "" {
		return nil, fmt.Errorf("s3 uploader missing bucket")
	}

	region := cfg.Region
	loadOpts := []func(*awsconfig.LoadOptions) error{}
	if region != "" {
		loadOpts = append(loadOpts, awsconfig.WithRegion(region))
	}
	if cfg.AccessKeyID != "" && cfg.SecretAccessKey != "" {
		loadOpts = append(loadOpts, awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
			cfg.AccessKeyID, cfg.SecretAccessKey, "",
		)))
	}
	if cfg.Endpoint != "" {
		customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) {
			return aws.Endpoint{
				URL:           strings.TrimRight(cfg.Endpoint, "/"),
				SigningRegion: cfg.Region,
			}, nil
		})
		loadOpts = append(loadOpts, awsconfig.WithEndpointResolverWithOptions(customResolver))
	}

	awsCfg, err := awsconfig.LoadDefaultConfig(ctx, loadOpts...)
	if err != nil {
		return nil, fmt.Errorf("load aws config: %w", err)
	}

	if region == "" {
		region = awsCfg.Region
	}
	if region == "" {
		return nil, fmt.Errorf("s3 uploader missing region")
	}

	client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
		o.UsePathStyle = cfg.UsePathStyle
		if cfg.Endpoint != "" {
			o.BaseEndpoint = aws.String(strings.TrimRight(cfg.Endpoint, "/"))
		}
	})

	// 预签名 URL 会交给外部客户端(浏览器 / MCP / curl)直连对象存储:
	// 配置了 PublicEndpoint 时用它签名(签名绑定 Host, 事后改写 URL 会验签失败),
	// 服务端自身的 Head/Get/Delete 仍走内部 Endpoint。
	presignClient := client
	if pub := strings.TrimRight(cfg.PublicEndpoint, "/"); pub != "" {
		pubOpts := []func(*awsconfig.LoadOptions) error{awsconfig.WithRegion(region)}
		if cfg.AccessKeyID != "" && cfg.SecretAccessKey != "" {
			pubOpts = append(pubOpts, awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
				cfg.AccessKeyID, cfg.SecretAccessKey, "",
			)))
		}
		pubCfg, err := awsconfig.LoadDefaultConfig(ctx, pubOpts...)
		if err != nil {
			return nil, fmt.Errorf("load aws config for public endpoint: %w", err)
		}
		presignClient = s3.NewFromConfig(pubCfg, func(o *s3.Options) {
			o.UsePathStyle = cfg.UsePathStyle
			o.BaseEndpoint = aws.String(pub)
		})
	}
	presign := s3.NewPresignClient(presignClient)

	prefix := strings.TrimLeft(cfg.UploadPrefix, "/")
	if prefix != "" && !strings.HasSuffix(prefix, "/") {
		prefix += "/"
	}

	baseURL := strings.TrimRight(cfg.BaseURL, "/")

	ttl := cfg.PresignTTL
	if ttl <= 0 {
		ttl = 15 * time.Minute
	}

	return &S3Uploader{
		client:     client,
		presign:    presign,
		bucket:     cfg.Bucket,
		baseURL:    baseURL,
		keyPrefix:  prefix,
		presignTTL: ttl,
	}, nil
}

func (u *S3Uploader) PresignPut(ctx context.Context, filename string, contentType string) (*UploadResult, error) {
	if filename == "" {
		filename = "upload.bin"
	}
	if contentType == "" {
		contentType = "application/octet-stream"
	}

	key := u.buildObjectKey(filename)
	req, err := u.presign.PresignPutObject(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(u.bucket),
		Key:         aws.String(key),
		ContentType: aws.String(contentType),
	}, s3.WithPresignExpires(u.presignTTL))
	if err != nil {
		return nil, fmt.Errorf("presign put: %w", err)
	}

	return &UploadResult{
		UploadURL: req.URL,
		Key:       key,
		ExpiresIn: int64(u.presignTTL.Seconds()),
	}, nil
}

func (u *S3Uploader) PresignGet(ctx context.Context, key string) (*SignedURLResult, error) {
	if key == "" {
		return nil, fmt.Errorf("presign get: missing key")
	}
	req, err := u.presign.PresignGetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(u.bucket),
		Key:    aws.String(key),
	}, s3.WithPresignExpires(u.presignTTL))
	if err != nil {
		return nil, fmt.Errorf("presign get: %w", err)
	}
	return &SignedURLResult{
		URL:       req.URL,
		ExpiresIn: int64(u.presignTTL.Seconds()),
	}, nil
}

func (u *S3Uploader) ObjectURL(key string) string {
	if key == "" {
		return ""
	}
	base := strings.TrimRight(u.baseURL, "/")
	if base == "" {
		return ""
	}
	return fmt.Sprintf("%s/%s", base, strings.TrimLeft(key, "/"))
}

func (u *S3Uploader) Bucket() string {
	return u.bucket
}

func (u *S3Uploader) Head(ctx context.Context, key string) (*ObjectInfo, error) {
	out, err := u.client.HeadObject(ctx, &s3.HeadObjectInput{
		Bucket: aws.String(u.bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return nil, fmt.Errorf("head object: %w", err)
	}
	return &ObjectInfo{
		ContentType: aws.ToString(out.ContentType),
		SizeBytes:   aws.ToInt64(out.ContentLength),
		ETag:        strings.Trim(aws.ToString(out.ETag), "\""),
	}, nil
}

func (u *S3Uploader) Get(ctx context.Context, key string) (io.ReadCloser, *ObjectInfo, error) {
	out, err := u.client.GetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(u.bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return nil, nil, fmt.Errorf("get object: %w", err)
	}
	return out.Body, &ObjectInfo{
		ContentType: aws.ToString(out.ContentType),
		SizeBytes:   aws.ToInt64(out.ContentLength),
		ETag:        strings.Trim(aws.ToString(out.ETag), "\""),
	}, nil
}

func (u *S3Uploader) Delete(ctx context.Context, key string) error {
	_, err := u.client.DeleteObject(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(u.bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return fmt.Errorf("delete object: %w", err)
	}
	return nil
}

func (u *S3Uploader) buildObjectKey(filename string) string {
	safeName := sanitizeFilename(filename)
	datePrefix := time.Now().Format("2006/01/02")
	return path.Join(u.keyPrefix, datePrefix, fmt.Sprintf("%s-%s", uuid.NewString(), safeName))
}

func sanitizeFilename(name string) string {
	if name == "" {
		return "upload.bin"
	}
	name = path.Base(name)
	invalidChars := regexp.MustCompile(`[^a-zA-Z0-9._-]+`)
	name = invalidChars.ReplaceAllString(name, "-")
	name = strings.Trim(name, "-")
	if name == "" {
		return "upload.bin"
	}
	return name
}
