package storage

import (
	"encoding/xml"
	"path"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/minio_ext"
	"code.gitea.io/gitea/modules/setting"

	miniov6 "github.com/minio/minio-go/v6"
)

const (
	PresignedUploadPartUrlExpireTime = time.Hour * 24 * 7
)

type ComplPart struct {
	PartNumber int    `json:"partNumber"`
	ETag       string `json:"eTag"`
}

type CompleteParts struct {
	Data []ComplPart `json:"completedParts"`
}

// completedParts is a collection of parts sortable by their part numbers.
// used for sorting the uploaded parts before completing the multipart request.
type completedParts []miniov6.CompletePart

func (a completedParts) Len() int           { return len(a) }
func (a completedParts) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a completedParts) Less(i, j int) bool { return a[i].PartNumber < a[j].PartNumber }

// completeMultipartUpload container for completing multipart upload.
type completeMultipartUpload struct {
	XMLName xml.Name               `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUpload" json:"-"`
	Parts   []miniov6.CompletePart `xml:"Part"`
}

var (
	adminClient *minio_ext.Client = nil
	coreClient  *miniov6.Core     = nil
)

var mutex *sync.Mutex

func init() {
	mutex = new(sync.Mutex)
}

func getClients() (*minio_ext.Client, *miniov6.Core, error) {
	var client *minio_ext.Client
	var core *miniov6.Core
	mutex.Lock()

	defer mutex.Unlock()

	if nil != adminClient && nil != coreClient {
		client = adminClient
		core = coreClient
		return client, core, nil
	}

	var err error
	minio := setting.Attachment.Minio
	if nil == adminClient {
		adminClient, err = minio_ext.New(
			minio.Endpoint,
			minio.AccessKeyID,
			minio.SecretAccessKey,
			minio.UseSSL,
		)
		if nil != err {
			return nil, nil, err
		}
	}

	client = adminClient

	if nil == coreClient {
		coreClient, err = miniov6.NewCore(
			minio.Endpoint,
			minio.AccessKeyID,
			minio.SecretAccessKey,
			minio.UseSSL,
		)
		if nil != err {
			return nil, nil, err
		}
	}

	core = coreClient

	return client, core, nil
}

func GenMultiPartSignedUrl(uuid string, uploadId string, partNumber int, partSize int64) (string, error) {
	minioClient, _, err := getClients()
	if err != nil {
		log.Error("getClients failed:", err.Error())
		return "", err
	}

	minio := setting.Attachment.Minio
	bucketName := minio.Bucket
	objectName := strings.TrimPrefix(path.Join(minio.BasePath, path.Join(uuid[0:1], uuid[1:2], uuid)), "/")

	return minioClient.GenUploadPartSignedUrl(uploadId, bucketName, objectName, partNumber, partSize, PresignedUploadPartUrlExpireTime, setting.Attachment.Minio.Location)

}

func NewMultiPartUpload(uuid string) (string, error) {
	_, core, err := getClients()
	if err != nil {
		log.Error("getClients failed:", err.Error())
		return "", err
	}

	minio := setting.Attachment.Minio
	bucketName := minio.Bucket
	objectName := strings.TrimPrefix(path.Join(minio.BasePath, path.Join(uuid[0:1], uuid[1:2], uuid)), "/")

	return core.NewMultipartUpload(bucketName, objectName, miniov6.PutObjectOptions{})
}

func CompleteMultiPartUpload(uuid string, uploadID string) (string, error) {
	client, core, err := getClients()
	if err != nil {
		log.Error("getClients failed:", err.Error())
		return "", err
	}

	minio := setting.Attachment.Minio
	bucketName := minio.Bucket
	objectName := strings.TrimPrefix(path.Join(minio.BasePath, path.Join(uuid[0:1], uuid[1:2], uuid)), "/")

	partInfos, err := client.ListObjectParts(bucketName, objectName, uploadID)
	if err != nil {
		log.Error("ListObjectParts failed:", err.Error())
		return "", err
	}

	var complMultipartUpload completeMultipartUpload
	for _, partInfo := range partInfos {
		complMultipartUpload.Parts = append(complMultipartUpload.Parts, miniov6.CompletePart{
			PartNumber: partInfo.PartNumber,
			ETag:       partInfo.ETag,
		})
	}

	// Sort all completed parts.
	sort.Sort(completedParts(complMultipartUpload.Parts))

	return core.CompleteMultipartUpload(bucketName, objectName, uploadID, complMultipartUpload.Parts)
}

func GetPartInfos(uuid string, uploadID string) (string, error) {
	minioClient, _, err := getClients()
	if err != nil {
		log.Error("getClients failed:", err.Error())
		return "", err
	}

	minio := setting.Attachment.Minio
	bucketName := minio.Bucket
	objectName := strings.TrimPrefix(path.Join(minio.BasePath, path.Join(uuid[0:1], uuid[1:2], uuid)), "/")

	partInfos, err := minioClient.ListObjectParts(bucketName, objectName, uploadID)
	if err != nil {
		log.Error("ListObjectParts failed:", err.Error())
		return "", err
	}

	var chunks string
	for _, partInfo := range partInfos {
		chunks += strconv.Itoa(partInfo.PartNumber) + "-" + partInfo.ETag + ","
	}

	return chunks, nil
}
