//go:build !plan9 && !solaris && !js

package oracleobjectstorage

import (
	"context"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/oracle/oci-go-sdk/v65/common"
	"github.com/oracle/oci-go-sdk/v65/objectstorage"
	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/operations"
)

// ------------------------------------------------------------
// Command Interface Implementation
// ------------------------------------------------------------

const (
	operationRename        = "rename"
	operationListMultiPart = "list-multipart-uploads"
	operationCleanup       = "cleanup"
	operationRestore       = "restore"
)

var commandHelp = []fs.CommandHelp{{
	Name:  operationRename,
	Short: "change the name of an object.",
	Long: `This command can be used to rename a object.

Usage example:

` + "```console" + `
rclone backend rename oos:bucket relative-object-path-under-bucket object-new-name
` + "```",
	Opts: nil,
}, {
	Name:  operationListMultiPart,
	Short: "List the unfinished multipart uploads.",
	Long: `This command lists the unfinished multipart uploads in JSON format.

Usage example:

` + "```console" + `
rclone backend list-multipart-uploads oos:bucket/path/to/object
` + "```" + `

It returns a dictionary of buckets with values as lists of unfinished
multipart uploads.

You can call it with no bucket in which case it lists all bucket, with
a bucket or with a bucket and path.

` + "```json" + `
{
    "test-bucket": [
        {
            "namespace": "test-namespace",
            "bucket": "test-bucket",
            "object": "600m.bin",
            "uploadId": "51dd8114-52a4-b2f2-c42f-5291f05eb3c8",
            "timeCreated": "2022-07-29T06:21:16.595Z",
            "storageTier": "Standard"
        }
    ]
}`,
}, {
	Name:  operationCleanup,
	Short: "Remove unfinished multipart uploads.",
	Long: `This command removes unfinished multipart uploads of age greater than
max-age which defaults to 24 hours.

Note that you can use --interactive/-i or --dry-run with this command to see
what it would do.

Usage examples:

` + "```console" + `
rclone backend cleanup oos:bucket/path/to/object
rclone backend cleanup -o max-age=7w oos:bucket/path/to/object
` + "```" + `

Durations are parsed as per the rest of rclone, 2h, 7d, 7w etc.`,
	Opts: map[string]string{
		"max-age": "Max age of upload to delete.",
	},
}, {
	Name:  operationRestore,
	Short: "Restore objects from Archive to Standard storage.",
	Long: `This command can be used to restore one or more objects from Archive to
Standard storage.

Usage examples:

` + "```console" + `
rclone backend restore oos:bucket/path/to/directory -o hours=HOURS
rclone backend restore oos:bucket -o hours=HOURS
` + "```" + `

This flag also obeys the filters. Test first with --interactive/-i or --dry-run flags

` + "```console" + `
rclone --interactive backend restore --include "*.txt" oos:bucket/path -o hours=72
` + "```" + `

All the objects shown will be marked for restore, then:

` + "```console" + `
rclone backend restore --include "*.txt" oos:bucket/path -o hours=72
` + "```" + `

It returns a list of status dictionaries with Object Name and Status keys.
The Status will be "RESTORED"" if it was successful or an error message if not.

` + "```json" + `
[
    {
        "Object": "test.txt"
        "Status": "RESTORED",
    },
    {
        "Object": "test/file4.txt"
        "Status": "RESTORED",
    }
]
` + "```",
	Opts: map[string]string{
		"hours": `The number of hours for which this object will be restored.
Default is 24 hrs.`,
	},
},
}

/*
Command the backend to run a named command

The command run is name
args may be used to read arguments from
opts may be used to read optional arguments from

The result should be capable of being JSON encoded
If it is a string or a []string it will be shown to the user
otherwise it will be JSON encoded and shown to the user like that
*/
func (f *Fs) Command(ctx context.Context, commandName string, args []string,
	opt map[string]string) (result any, err error) {
	// fs.Debugf(f, "command %v, args: %v, opts:%v", commandName, args, opt)
	switch commandName {
	case operationRename:
		if len(args) < 2 {
			return nil, fmt.Errorf("path to object or its new name to rename is empty")
		}
		remote := args[0]
		newName := args[1]
		return f.rename(ctx, remote, newName)
	case operationListMultiPart:
		return f.listMultipartUploadsAll(ctx)
	case operationCleanup:
		maxAge := 24 * time.Hour
		if opt["max-age"] != "" {
			maxAge, err = fs.ParseDuration(opt["max-age"])
			if err != nil {
				return nil, fmt.Errorf("bad max-age: %w", err)
			}
		}
		return nil, f.cleanUp(ctx, maxAge)
	case operationRestore:
		return f.restore(ctx, opt)
	default:
		return nil, fs.ErrorCommandNotFound
	}
}

func (f *Fs) rename(ctx context.Context, remote, newName string) (any, error) {
	if remote == "" {
		return nil, fmt.Errorf("path to object file cannot be empty")
	}
	if newName == "" {
		return nil, fmt.Errorf("the object's new name cannot be empty")
	}
	o := &Object{
		fs:     f,
		remote: remote,
	}
	bucketName, objectPath := o.split()
	err := o.readMetaData(ctx)
	if err != nil {
		fs.Errorf(f, "failed to read object:%v %v ", objectPath, err)
		if strings.HasPrefix(objectPath, bucketName) {
			fs.Errorf(f, "warn: ensure object path: %v is relative to bucket:%v and doesn't include the bucket name",
				objectPath, bucketName)
		}
		return nil, fs.ErrorNotAFile
	}
	details := objectstorage.RenameObjectDetails{
		SourceName: common.String(objectPath),
		NewName:    common.String(newName),
	}
	request := objectstorage.RenameObjectRequest{
		NamespaceName:       common.String(f.opt.Namespace),
		BucketName:          common.String(bucketName),
		RenameObjectDetails: details,
		OpcClientRequestId:  nil,
		RequestMetadata:     common.RequestMetadata{},
	}
	var response objectstorage.RenameObjectResponse
	err = f.pacer.Call(func() (bool, error) {
		response, err = f.srv.RenameObject(ctx, request)
		return shouldRetry(ctx, response.HTTPResponse(), err)
	})
	if err != nil {
		return nil, err
	}
	fs.Infof(f, "success: renamed object-path: %v to %v", objectPath, newName)
	return "renamed successfully", nil
}

func (f *Fs) listMultipartUploadsAll(ctx context.Context) (uploadsMap map[string][]*objectstorage.MultipartUpload,
	err error) {
	uploadsMap = make(map[string][]*objectstorage.MultipartUpload)
	bucket, directory := f.split("")
	if bucket != "" {
		uploads, err := f.listMultipartUploads(ctx, bucket, directory)
		if err != nil {
			return uploadsMap, err
		}
		uploadsMap[bucket] = uploads
		return uploadsMap, nil
	}
	entries, err := f.listBuckets(ctx)
	if err != nil {
		return uploadsMap, err
	}
	for _, entry := range entries {
		bucket := entry.Remote()
		uploads, listErr := f.listMultipartUploads(ctx, bucket, "")
		if listErr != nil {
			err = listErr
			fs.Errorf(f, "%v", err)
		}
		uploadsMap[bucket] = uploads
	}
	return uploadsMap, err
}

// listMultipartUploads lists all outstanding multipart uploads for (bucket, key)
//
// Note that rather lazily we treat key as a prefix, so it matches
// directories and objects. This could surprise the user if they ask
// for "dir" and it returns "dirKey"
func (f *Fs) listMultipartUploads(ctx context.Context, bucketName, directory string) (
	uploads []*objectstorage.MultipartUpload, err error) {
	return f.listMultipartUploadsObject(ctx, bucketName, directory, false)
}

// listMultipartUploads finds first outstanding multipart uploads for (bucket, key)
//
// Note that rather lazily we treat key as a prefix, so it matches
// directories and objects. This could surprise the user if they ask
// for "dir" and it returns "dirKey"
func (f *Fs) findLatestMultipartUpload(ctx context.Context, bucketName, directory string) (
	uploads []*objectstorage.MultipartUpload, err error) {
	pastUploads, err := f.listMultipartUploadsObject(ctx, bucketName, directory, true)
	if err != nil {
		return nil, err
	}

	if len(pastUploads) > 0 {
		sort.Slice(pastUploads, func(i, j int) bool {
			return pastUploads[i].TimeCreated.After(pastUploads[j].TimeCreated.Time)
		})
		return pastUploads[:1], nil
	}
	return nil, err
}

func (f *Fs) listMultipartUploadsObject(ctx context.Context, bucketName, directory string, exact bool) (
	uploads []*objectstorage.MultipartUpload, err error) {

	uploads = []*objectstorage.MultipartUpload{}
	req := objectstorage.ListMultipartUploadsRequest{
		NamespaceName: common.String(f.opt.Namespace),
		BucketName:    common.String(bucketName),
	}

	var response objectstorage.ListMultipartUploadsResponse
	for {
		err = f.pacer.Call(func() (bool, error) {
			response, err = f.srv.ListMultipartUploads(ctx, req)
			return shouldRetry(ctx, response.HTTPResponse(), err)
		})
		if err != nil {
			// fs.Debugf(f, "failed to list multi part uploads %v", err)
			return uploads, err
		}
		for index, item := range response.Items {
			if directory != "" && item.Object != nil && !strings.HasPrefix(*item.Object, directory) {
				continue
			}
			if exact {
				if *item.Object == directory {
					uploads = append(uploads, &response.Items[index])
				}
			} else {
				uploads = append(uploads, &response.Items[index])
			}
		}
		if response.OpcNextPage == nil {
			break
		}
		req.Page = response.OpcNextPage
	}
	return uploads, nil
}

func (f *Fs) listMultipartUploadParts(ctx context.Context, bucketName, bucketPath string, uploadID string) (
	uploadedParts map[int]objectstorage.MultipartUploadPartSummary, err error) {
	uploadedParts = make(map[int]objectstorage.MultipartUploadPartSummary)
	req := objectstorage.ListMultipartUploadPartsRequest{
		NamespaceName: common.String(f.opt.Namespace),
		BucketName:    common.String(bucketName),
		ObjectName:    common.String(bucketPath),
		UploadId:      common.String(uploadID),
		Limit:         common.Int(1000),
	}

	var response objectstorage.ListMultipartUploadPartsResponse
	for {
		err = f.pacer.Call(func() (bool, error) {
			response, err = f.srv.ListMultipartUploadParts(ctx, req)
			return shouldRetry(ctx, response.HTTPResponse(), err)
		})
		if err != nil {
			return uploadedParts, err
		}
		for _, item := range response.Items {
			uploadedParts[*item.PartNumber] = item
		}
		if response.OpcNextPage == nil {
			break
		}
		req.Page = response.OpcNextPage
	}
	return uploadedParts, nil
}

func (f *Fs) restore(ctx context.Context, opt map[string]string) (any, error) {
	req := objectstorage.RestoreObjectsRequest{
		NamespaceName:         common.String(f.opt.Namespace),
		RestoreObjectsDetails: objectstorage.RestoreObjectsDetails{},
	}
	if hours := opt["hours"]; hours != "" {
		ihours, err := strconv.Atoi(hours)
		if err != nil {
			return nil, fmt.Errorf("bad value for hours: %w", err)
		}
		req.RestoreObjectsDetails.Hours = &ihours
	}
	type status struct {
		Object string
		Status string
	}
	var (
		outMu sync.Mutex
		out   = []status{}
		err   error
	)
	err = operations.ListFn(ctx, f, func(obj fs.Object) {
		// Remember this is run --checkers times concurrently
		o, ok := obj.(*Object)
		st := status{Object: obj.Remote(), Status: "RESTORED"}
		defer func() {
			outMu.Lock()
			out = append(out, st)
			outMu.Unlock()
		}()
		if !ok {
			st.Status = "Not an OCI Object Storage object"
			return
		}
		if o.storageTier == nil || (*o.storageTier != "archive") {
			st.Status = "Object not in Archive storage tier"
			return
		}
		if operations.SkipDestructive(ctx, obj, "restore") {
			return
		}
		bucket, bucketPath := o.split()
		reqCopy := req
		reqCopy.BucketName = &bucket
		reqCopy.ObjectName = &bucketPath
		var response objectstorage.RestoreObjectsResponse
		err = f.pacer.Call(func() (bool, error) {
			response, err = f.srv.RestoreObjects(ctx, reqCopy)
			return shouldRetry(ctx, response.HTTPResponse(), err)
		})
		if err != nil {
			st.Status = err.Error()
		}
	})
	if err != nil {
		return out, err
	}
	return out, nil
}
