// Package filelu provides an interface to the FileLu storage system.
package filelu

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"strings"
	"time"

	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/config"
	"github.com/rclone/rclone/fs/config/configmap"
	"github.com/rclone/rclone/fs/config/configstruct"
	"github.com/rclone/rclone/fs/fshttp"
	"github.com/rclone/rclone/lib/encoder"
	"github.com/rclone/rclone/lib/pacer"
	"github.com/rclone/rclone/lib/rest"
)

const (
	defaultUploadCutoff = fs.SizeSuffix(500 * 1024 * 1024)
	defaultChunkSize    = fs.SizeSuffix(64 * 1024 * 1024)
)

// Register the backend with Rclone
func init() {
	fs.Register(&fs.RegInfo{
		Name:        "filelu",
		Description: "FileLu Cloud Storage",
		NewFs:       NewFs,
		Options: []fs.Option{{
			Name:      "key",
			Help:      "Your FileLu Rclone key from My Account",
			Required:  true,
			Sensitive: true,
		},
			{
				Name:     "upload_cutoff",
				Help:     "Cutoff for switching to chunked upload. Any files larger than this will be uploaded in chunks of chunk_size.",
				Default:  defaultUploadCutoff,
				Advanced: true,
			}, {
				Name:     "chunk_size",
				Help:     "Chunk size to use for uploading. Used for multipart uploads.",
				Default:  defaultChunkSize,
				Advanced: true,
			},
			{
				Name:     config.ConfigEncoding,
				Help:     config.ConfigEncodingHelp,
				Advanced: true,
				Default: (encoder.Base | //  Slash,LtGt,DoubleQuote,Question,Asterisk,Pipe,Hash,Percent,BackSlash,Del,Ctl,RightSpace,InvalidUtf8,Dot
					encoder.EncodeSlash |
					encoder.EncodeLtGt |
					encoder.EncodeExclamation |
					encoder.EncodeDoubleQuote |
					encoder.EncodeSingleQuote |
					encoder.EncodeBackQuote |
					encoder.EncodeQuestion |
					encoder.EncodeDollar |
					encoder.EncodeColon |
					encoder.EncodeAsterisk |
					encoder.EncodePipe |
					encoder.EncodeHash |
					encoder.EncodePercent |
					encoder.EncodeBackSlash |
					encoder.EncodeCrLf |
					encoder.EncodeDel |
					encoder.EncodeCtl |
					encoder.EncodeLeftSpace |
					encoder.EncodeLeftPeriod |
					encoder.EncodeLeftTilde |
					encoder.EncodeLeftCrLfHtVt |
					encoder.EncodeRightPeriod |
					encoder.EncodeRightCrLfHtVt |
					encoder.EncodeSquareBracket |
					encoder.EncodeSemicolon |
					encoder.EncodeRightSpace |
					encoder.EncodeInvalidUtf8 |
					encoder.EncodeDot),
			},
		}})
}

// Options defines the configuration for the FileLu backend
type Options struct {
	Key          string               `config:"key"`
	Enc          encoder.MultiEncoder `config:"encoding"`
	UploadCutoff fs.SizeSuffix        `config:"upload_cutoff"`
	ChunkSize    fs.SizeSuffix        `config:"chunk_size"`
}

// Fs represents the FileLu file system
type Fs struct {
	name       string
	root       string
	opt        Options
	features   *fs.Features
	endpoint   string
	pacer      *pacer.Pacer
	srv        *rest.Client
	client     *http.Client
	targetFile string
}

// NewFs creates a new Fs object for FileLu
func NewFs(ctx context.Context, name string, root string, m configmap.Mapper) (fs.Fs, error) {
	opt := new(Options)
	err := configstruct.Set(m, opt)
	if err != nil {
		return nil, fmt.Errorf("failed to parse config: %w", err)
	}

	if opt.Key == "" {
		return nil, fmt.Errorf("FileLu Rclone Key is required")
	}

	client := fshttp.NewClient(ctx)

	if strings.TrimSpace(root) == "" {
		root = ""
	}
	root = strings.Trim(root, "/")

	filename := ""

	f := &Fs{
		name:       name,
		opt:        *opt,
		endpoint:   "https://filelu.com/rclone",
		client:     client,
		srv:        rest.NewClient(client).SetRoot("https://filelu.com/rclone"),
		pacer:      pacer.New(),
		targetFile: filename,
		root:       root,
	}

	f.features = (&fs.Features{
		CanHaveEmptyDirectories: true,
		WriteMetadata:           false,
		SlowHash:                true,
	}).Fill(ctx, f)

	rootContainer, rootDirectory := rootSplit(f.root)
	if rootContainer != "" && rootDirectory != "" {
		// Check to see if the (container,directory) is actually an existing file
		oldRoot := f.root
		newRoot, leaf := path.Split(oldRoot)
		f.root = strings.Trim(newRoot, "/")
		_, err := f.NewObject(ctx, leaf)
		if err != nil {
			if err == fs.ErrorObjectNotFound || err == fs.ErrorNotAFile {
				// File doesn't exist or is a directory so return old f
				f.root = strings.Trim(oldRoot, "/")
				return f, nil
			}
			return nil, err
		}
		// return an error with an fs which points to the parent
		return f, fs.ErrorIsFile
	}

	return f, nil
}

// Mkdir to create directory on remote server.
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
	fullPath := path.Clean(f.root + "/" + dir)
	_, err := f.createFolder(ctx, fullPath)
	return err
}

// About provides usage statistics for the remote
func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
	accountInfo, err := f.getAccountInfo(ctx)
	if err != nil {
		return nil, err
	}

	totalStorage, err := parseStorageToBytes(accountInfo.Result.Storage)
	if err != nil {
		return nil, fmt.Errorf("failed to parse total storage: %w", err)
	}

	usedStorage, err := parseStorageToBytes(accountInfo.Result.StorageUsed)
	if err != nil {
		return nil, fmt.Errorf("failed to parse used storage: %w", err)
	}

	return &fs.Usage{
		Total: fs.NewUsageValue(totalStorage), // Total bytes available
		Used:  fs.NewUsageValue(usedStorage),  // Total bytes used
		Free:  fs.NewUsageValue(totalStorage - usedStorage),
	}, nil
}

// Purge deletes the directory and all its contents
func (f *Fs) Purge(ctx context.Context, dir string) error {
	fullPath := path.Join(f.root, dir)
	if fullPath != "" {
		fullPath = "/" + strings.Trim(fullPath, "/")
	}
	return f.deleteFolder(ctx, fullPath)
}

// List returns a list of files and folders for the given directory
func (f *Fs) List(ctx context.Context, dir string) (fs.DirEntries, error) {
	// Compose full path for API call
	fullPath := path.Join(f.root, dir)
	fullPath = "/" + strings.Trim(fullPath, "/")
	if fullPath == "/" {
		fullPath = ""
	}

	var entries fs.DirEntries
	result, err := f.getFolderList(ctx, fullPath)
	if err != nil {
		return nil, err
	}

	fldMap := map[string]bool{}
	for _, folder := range result.Result.Folders {
		fldMap[folder.FldID.String()] = true
		if f.root == "" && dir == "" && strings.Contains(folder.Path, "/") {
			continue
		}

		paths := strings.Split(folder.Path, fullPath+"/")
		remote := paths[0]
		if len(paths) > 1 {
			remote = paths[1]
		}

		if strings.Contains(remote, "/") {
			continue
		}

		pathsWithoutRoot := strings.Split(folder.Path, "/"+f.root+"/")
		remotePathWithoutRoot := pathsWithoutRoot[0]
		if len(pathsWithoutRoot) > 1 {
			remotePathWithoutRoot = pathsWithoutRoot[1]
		}
		remotePathWithoutRoot = strings.TrimPrefix(remotePathWithoutRoot, "/")
		entries = append(entries, fs.NewDir(remotePathWithoutRoot, time.Now()))
	}
	for _, file := range result.Result.Files {
		if _, ok := fldMap[file.FldID.String()]; ok {
			continue
		}
		remote := path.Join(dir, file.Name)
		// trim leading slashes
		remote = strings.TrimPrefix(remote, "/")
		obj := &Object{
			fs:      f,
			remote:  remote,
			size:    file.Size,
			modTime: time.Now(),
		}
		entries = append(entries, obj)
	}
	return entries, nil
}

// Put uploads a file directly to the destination folder in the FileLu storage system.
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
	o := &Object{
		fs:     f,
		remote: src.Remote(),
	}
	return o, o.Update(ctx, in, src, options...)
}

// Move moves the file to the specified location
func (f *Fs) Move(ctx context.Context, src fs.Object, destinationPath string) (fs.Object, error) {

	if strings.HasPrefix(destinationPath, "/") || strings.Contains(destinationPath, ":\\") {
		dir := path.Dir(destinationPath)
		if err := os.MkdirAll(dir, 0755); err != nil {
			return nil, fmt.Errorf("failed to create destination directory: %w", err)
		}

		reader, err := src.Open(ctx)
		if err != nil {
			return nil, fmt.Errorf("failed to open source file: %w", err)
		}
		defer func() {
			if err := reader.Close(); err != nil {
				fs.Logf(nil, "Failed to close file body: %v", err)
			}
		}()

		dest, err := os.Create(destinationPath)
		if err != nil {
			return nil, fmt.Errorf("failed to create destination file: %w", err)
		}
		defer func() {
			if err := dest.Close(); err != nil {
				fs.Logf(nil, "Failed to close file body: %v", err)
			}
		}()

		if _, err := io.Copy(dest, reader); err != nil {
			return nil, fmt.Errorf("failed to copy file content: %w", err)
		}

		if err := src.Remove(ctx); err != nil {
			return nil, fmt.Errorf("failed to remove source file: %w", err)
		}

		return nil, nil
	}

	reader, err := src.Open(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to open source object: %w", err)
	}
	defer func() {
		if err := reader.Close(); err != nil {
			fs.Logf(nil, "Failed to close file body: %v", err)
		}
	}()

	err = f.uploadFile(ctx, reader, destinationPath)
	if err != nil {
		return nil, fmt.Errorf("failed to upload file to destination: %w", err)
	}

	if err := src.Remove(ctx); err != nil {
		return nil, fmt.Errorf("failed to delete source file: %w", err)
	}

	return &Object{
		fs:      f,
		remote:  destinationPath,
		size:    src.Size(),
		modTime: src.ModTime(ctx),
	}, nil
}

// Rmdir removes a directory
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
	fullPath := path.Join(f.root, dir)
	if fullPath != "" {
		fullPath = "/" + strings.Trim(fullPath, "/")
	}

	// Step 1: Check if folder is empty
	listResp, err := f.getFolderList(ctx, fullPath)
	if err != nil {
		return err
	}
	if len(listResp.Result.Files) > 0 || len(listResp.Result.Folders) > 0 {
		return fmt.Errorf("Rmdir: directory %q is not empty", fullPath)
	}

	// Step 2: Delete the folder
	return f.deleteFolder(ctx, fullPath)
}

// Check the interfaces are satisfied
var (
	_ fs.Fs      = (*Fs)(nil)
	_ fs.Purger  = (*Fs)(nil)
	_ fs.Abouter = (*Fs)(nil)
	_ fs.Mover   = (*Fs)(nil)
	_ fs.Object  = (*Object)(nil)
)
