package storage

import (
	"context"
	"fmt"
	"io"
	"net/url"
	"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/aws/aws-sdk-go-v2/service/s3/types"
	"github.com/perfect-panel/server/internal/config"
)

type PresignUploadResult struct {
	URL       string
	Method    string
	ExpiresAt int64
}

type ObjectMeta struct {
	ETag          string
	ContentType   string
	ContentLength int64
}

type PutObjectResult struct {
	ETag string
}

type S3Store struct {
	cfg     config.S3Config
	client  *s3.Client
	presign *s3.PresignClient
}

func NewS3Store(ctx context.Context, cfg config.S3Config) (*S3Store, error) {
	loaders := make([]func(*awsconfig.LoadOptions) error, 0, 3)
	if cfg.Region != "" {
		loaders = append(loaders, awsconfig.WithRegion(cfg.Region))
	}
	if cfg.AccessKey != "" || cfg.SecretKey != "" || cfg.SessionToken != "" {
		loaders = append(loaders, awsconfig.WithCredentialsProvider(
			credentials.NewStaticCredentialsProvider(cfg.AccessKey, cfg.SecretKey, cfg.SessionToken),
		))
	}

	awsCfg, err := awsconfig.LoadDefaultConfig(ctx, loaders...)
	if err != nil {
		return nil, err
	}

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

	return &S3Store{
		cfg:     cfg,
		client:  client,
		presign: s3.NewPresignClient(client),
	}, nil
}

func (s *S3Store) PresignPutObject(ctx context.Context, objectKey string, contentType string, expires time.Duration) (*PresignUploadResult, error) {
	req, err := s.presign.PresignPutObject(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(s.cfg.Bucket),
		Key:         aws.String(objectKey),
		ContentType: aws.String(contentType),
	}, s3.WithPresignExpires(expires))
	if err != nil {
		return nil, err
	}

	return &PresignUploadResult{
		URL:       req.URL,
		Method:    req.Method,
		ExpiresAt: time.Now().Add(expires).Unix(),
	}, nil
}

func (s *S3Store) HeadObject(ctx context.Context, objectKey string) (*ObjectMeta, error) {
	resp, err := s.client.HeadObject(ctx, &s3.HeadObjectInput{
		Bucket: aws.String(s.cfg.Bucket),
		Key:    aws.String(objectKey),
	})
	if err != nil {
		return nil, err
	}

	return &ObjectMeta{
		ETag:          strings.Trim(aws.ToString(resp.ETag), "\""),
		ContentType:   aws.ToString(resp.ContentType),
		ContentLength: aws.ToInt64(resp.ContentLength),
	}, nil
}

func (s *S3Store) PutObject(ctx context.Context, objectKey string, body io.Reader, size int64, contentType string) (*PutObjectResult, error) {
	resp, err := s.client.PutObject(ctx, &s3.PutObjectInput{
		Bucket:        aws.String(s.cfg.Bucket),
		Key:           aws.String(objectKey),
		Body:          body,
		ContentLength: aws.Int64(size),
		ContentType:   aws.String(contentType),
	})
	if err != nil {
		return nil, err
	}
	return &PutObjectResult{ETag: strings.Trim(aws.ToString(resp.ETag), "\"")}, nil
}

func (s *S3Store) BuildObjectURL(objectKey string) string {
	if s.cfg.PublicBaseURL != "" {
		return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + strings.TrimLeft(objectKey, "/")
	}

	if s.cfg.Endpoint != "" {
		base := strings.TrimRight(s.cfg.Endpoint, "/")
		return fmt.Sprintf("%s/%s/%s", base, url.PathEscape(s.cfg.Bucket), strings.TrimLeft(objectKey, "/"))
	}

	return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", s.cfg.Bucket, s.cfg.Region, strings.TrimLeft(objectKey, "/"))
}

func (s *S3Store) PutJSONTagging(ctx context.Context, objectKey string, values map[string]string) error {
	tagging := make([]types.Tag, 0, len(values))
	for k, v := range values {
		key := k
		value := v
		tagging = append(tagging, types.Tag{Key: &key, Value: &value})
	}

	_, err := s.client.PutObjectTagging(ctx, &s3.PutObjectTaggingInput{
		Bucket: aws.String(s.cfg.Bucket),
		Key:    aws.String(objectKey),
		Tagging: &types.Tagging{
			TagSet: tagging,
		},
	})
	return err
}
