package client

import (
	"context"
	"crypto/tls"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
)

const (
	s3DefaultTimeout  = 30 * time.Second
	s3TransferTimeout = 24 * time.Hour
)

type s3Client struct {
	scType string
	bucket string
	client *minio.Client
}

func (s *s3Client) ctx(timeout time.Duration) (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.Background(), timeout)
}

func NewS3Client(vars map[string]interface{}) (*s3Client, error) {
	accessKey := loadParamFromVars("accessKey", vars)
	secretKey := loadParamFromVars("secretKey", vars)
	endpoint := loadParamFromVars("endpoint", vars)
	region := loadParamFromVars("region", vars)
	bucket := loadParamFromVars("bucket", vars)
	scType := loadParamFromVars("scType", vars)
	if len(scType) == 0 {
		scType = "Standard"
	}
	mode := loadParamFromVars("mode", vars)
	if len(mode) == 0 {
		mode = "virtual hosted"
	}

	lookupStyle := minio.BucketLookupDNS
	if mode == "path" {
		lookupStyle = minio.BucketLookupPath
	}

	ssl := strings.Split(endpoint, ":")[0]
	secure := false
	tlsConfig := &tls.Config{}
	if ssl == "https" {
		secure = true
		tlsConfig.InsecureSkipVerify = true
	}
	var transport http.RoundTripper = &http.Transport{
		TLSClientConfig: tlsConfig,
	}

	endpoint = strings.TrimPrefix(endpoint, ssl+"://")

	client, err := minio.New(endpoint, &minio.Options{
		Creds:        credentials.NewStaticV4(accessKey, secretKey, ""),
		Secure:       secure,
		Region:       region,
		BucketLookup: lookupStyle,
		Transport:    transport,
	})
	if err != nil {
		return nil, err
	}
	return &s3Client{scType: scType, bucket: bucket, client: client}, nil
}

func (s s3Client) ListBuckets() ([]interface{}, error) {
	ctx, cancel := s.ctx(s3DefaultTimeout)
	defer cancel()
	buckets, err := s.client.ListBuckets(ctx)
	if err != nil {
		return nil, err
	}
	var result []interface{}
	for _, b := range buckets {
		result = append(result, b.Name)
	}
	return result, nil
}

func (s s3Client) Exist(path string) (bool, error) {
	ctx, cancel := s.ctx(s3DefaultTimeout)
	defer cancel()
	_, err := s.client.StatObject(ctx, s.bucket, path, minio.StatObjectOptions{})
	if err != nil {
		resp := minio.ToErrorResponse(err)
		if resp.StatusCode == 404 {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

func (s *s3Client) Size(path string) (int64, error) {
	ctx, cancel := s.ctx(s3DefaultTimeout)
	defer cancel()
	info, err := s.client.StatObject(ctx, s.bucket, path, minio.StatObjectOptions{})
	if err != nil {
		return 0, err
	}
	return info.Size, nil
}

func (s s3Client) Delete(path string) (bool, error) {
	ctx, cancel := s.ctx(s3DefaultTimeout)
	defer cancel()
	if err := s.client.RemoveObject(ctx, s.bucket, path, minio.RemoveObjectOptions{}); err != nil {
		return false, err
	}
	return true, nil
}

func (s s3Client) Upload(ctx context.Context, src, target string) (bool, error) {
	fileInfo, err := os.Stat(src)
	if err != nil {
		return false, err
	}
	file, err := os.Open(src)
	if err != nil {
		return false, err
	}
	defer file.Close()

	opts := minio.PutObjectOptions{
		StorageClass: s.scType,
	}

	const maxParts = 10000
	const defaultPartSize = 64 * 1024 * 1024 // 64 MiB
	partSize := uint64(defaultPartSize)
	if fileInfo.Size() > int64(maxParts)*int64(defaultPartSize) {
		partSize = uint64(fileInfo.Size()) / (maxParts - 1)
	}
	opts.PartSize = partSize

	if _, ok := ctx.Deadline(); !ok {
		var cancel context.CancelFunc
		ctx, cancel = s.ctx(s3TransferTimeout)
		defer cancel()
	}
	if _, err := s.client.PutObject(ctx, s.bucket, target, file, fileInfo.Size(), opts); err != nil {
		return false, err
	}
	return true, nil
}

func (s s3Client) Download(src, target string) (bool, error) {
	if _, err := os.Stat(target); err == nil {
		_ = os.Remove(target)
	}
	ctx, cancel := s.ctx(s3TransferTimeout)
	defer cancel()
	if err := s.client.FGetObject(ctx, s.bucket, src, target, minio.GetObjectOptions{}); err != nil {
		os.Remove(target)
		return false, err
	}
	return true, nil
}

func (s *s3Client) ListObjects(prefix string) ([]string, error) {
	opts := minio.ListObjectsOptions{
		Recursive: true,
		Prefix:    prefix,
	}
	var result []string
	ctx, cancel := s.ctx(s3DefaultTimeout)
	defer cancel()
	for object := range s.client.ListObjects(ctx, s.bucket, opts) {
		if object.Err != nil {
			return result, object.Err
		}
		result = append(result, object.Key)
	}
	return result, nil
}
