// Package fichier provides an interface to the 1Fichier storage system.
package fichier

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"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/fs/hash"
	"github.com/rclone/rclone/lib/dircache"
	"github.com/rclone/rclone/lib/encoder"
	"github.com/rclone/rclone/lib/pacer"
	"github.com/rclone/rclone/lib/rest"
)

const (
	rootID         = "0"
	apiBaseURL     = "https://api.1fichier.com/v1"
	minSleep       = 400 * time.Millisecond // api is extremely rate limited now
	maxSleep       = 5 * time.Second
	decayConstant  = 2 // bigger for slower decay, exponential
	attackConstant = 0 // start with max sleep
)

func init() {
	fs.Register(&fs.RegInfo{
		Name:        "fichier",
		Description: "1Fichier",
		NewFs:       NewFs,
		Options: []fs.Option{{
			Help:      "Your API Key, get it from https://1fichier.com/console/params.pl.",
			Name:      "api_key",
			Sensitive: true,
		}, {
			Help:     "If you want to download a shared folder, add this parameter.",
			Name:     "shared_folder",
			Advanced: true,
		}, {
			Help:       "If you want to download a shared file that is password protected, add this parameter.",
			Name:       "file_password",
			Advanced:   true,
			IsPassword: true,
		}, {
			Help:       "If you want to list the files in a shared folder that is password protected, add this parameter.",
			Name:       "folder_password",
			Advanced:   true,
			IsPassword: true,
		}, {
			Help:     "Set if you wish to use CDN download links.",
			Name:     "cdn",
			Default:  false,
			Advanced: true,
		}, {
			Name:     config.ConfigEncoding,
			Help:     config.ConfigEncodingHelp,
			Advanced: true,
			// Characters that need escaping
			//
			// 		'\\': '＼', // FULLWIDTH REVERSE SOLIDUS
			// 		'<':  '＜', // FULLWIDTH LESS-THAN SIGN
			// 		'>':  '＞', // FULLWIDTH GREATER-THAN SIGN
			// 		'"':  '＂', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved
			// 		'\'': '＇', // FULLWIDTH APOSTROPHE
			// 		'$':  '＄', // FULLWIDTH DOLLAR SIGN
			// 		'`':  '｀', // FULLWIDTH GRAVE ACCENT
			//
			// Leading space and trailing space
			Default: (encoder.Display |
				encoder.EncodeBackSlash |
				encoder.EncodeSingleQuote |
				encoder.EncodeBackQuote |
				encoder.EncodeDoubleQuote |
				encoder.EncodeLtGt |
				encoder.EncodeDollar |
				encoder.EncodeLeftSpace |
				encoder.EncodeRightSpace |
				encoder.EncodeInvalidUtf8),
		}},
	})
}

// Options defines the configuration for this backend
type Options struct {
	APIKey         string               `config:"api_key"`
	SharedFolder   string               `config:"shared_folder"`
	FilePassword   string               `config:"file_password"`
	FolderPassword string               `config:"folder_password"`
	CDN            bool                 `config:"cdn"`
	Enc            encoder.MultiEncoder `config:"encoding"`
}

// Fs is the interface a cloud storage system must provide
type Fs struct {
	root       string
	name       string
	features   *fs.Features
	opt        Options
	dirCache   *dircache.DirCache
	baseClient *http.Client
	pacer      *fs.Pacer
	rest       *rest.Client
}

// FindLeaf finds a directory of name leaf in the folder with ID pathID
func (f *Fs) FindLeaf(ctx context.Context, pathID, leaf string) (pathIDOut string, found bool, err error) {
	folderID, err := strconv.Atoi(pathID)
	if err != nil {
		return "", false, err
	}
	folders, err := f.listFolders(ctx, folderID)
	if err != nil {
		return "", false, err
	}

	for _, folder := range folders.SubFolders {
		if folder.Name == leaf {
			pathIDOut := strconv.Itoa(folder.ID)
			return pathIDOut, true, nil
		}
	}

	return "", false, nil
}

// CreateDir makes a directory with pathID as parent and name leaf
func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (newID string, err error) {
	folderID, err := strconv.Atoi(pathID)
	if err != nil {
		return "", err
	}
	resp, err := f.makeFolder(ctx, leaf, folderID)
	if err != nil {
		return "", err
	}
	return strconv.Itoa(resp.FolderID), err
}

// Name of the remote (as passed into NewFs)
func (f *Fs) Name() string {
	return f.name
}

// Root of the remote (as passed into NewFs)
func (f *Fs) Root() string {
	return f.root
}

// String returns a description of the FS
func (f *Fs) String() string {
	return fmt.Sprintf("1Fichier root '%s'", f.root)
}

// Precision of the ModTimes in this Fs
func (f *Fs) Precision() time.Duration {
	return fs.ModTimeNotSupported
}

// Hashes returns the supported hash types of the filesystem
func (f *Fs) Hashes() hash.Set {
	return hash.Set(hash.Whirlpool)
}

// Features returns the optional features of this Fs
func (f *Fs) Features() *fs.Features {
	return f.features
}

// NewFs makes a new Fs object from the path
//
// The path is of the form remote:path
//
// Remotes are looked up in the config file.  If the remote isn't
// found then NotFoundInConfigFile will be returned.
//
// On Windows avoid single character remote names as they can be mixed
// up with drive letters.
func NewFs(ctx context.Context, name string, root string, config configmap.Mapper) (fs.Fs, error) {
	opt := new(Options)
	err := configstruct.Set(config, opt)
	if err != nil {
		return nil, err
	}

	// If using a Shared Folder override root
	if opt.SharedFolder != "" {
		root = ""
	}

	//workaround for wonky parser
	root = strings.Trim(root, "/")

	f := &Fs{
		name:       name,
		root:       root,
		opt:        *opt,
		pacer:      fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant), pacer.AttackConstant(attackConstant))),
		baseClient: &http.Client{},
	}

	f.features = (&fs.Features{
		DuplicateFiles:          true,
		CanHaveEmptyDirectories: true,
		ReadMimeType:            true,
	}).Fill(ctx, f)

	client := fshttp.NewClient(ctx)

	f.rest = rest.NewClient(client).SetRoot(apiBaseURL)

	f.rest.SetHeader("Authorization", "Bearer "+f.opt.APIKey)

	f.dirCache = dircache.New(root, rootID, f)

	// Find the current root
	err = f.dirCache.FindRoot(ctx, false)
	if err != nil {
		// Assume it is a file
		newRoot, remote := dircache.SplitPath(root)
		tempF := *f
		tempF.dirCache = dircache.New(newRoot, rootID, &tempF)
		tempF.root = newRoot
		// Make new Fs which is the parent
		err = tempF.dirCache.FindRoot(ctx, false)
		if err != nil {
			// No root so return old f
			return f, nil
		}
		_, err := tempF.NewObject(ctx, remote)
		if err != nil {
			if err == fs.ErrorObjectNotFound {
				// File doesn't exist so return old f
				return f, nil
			}
			return nil, err
		}
		f.features.Fill(ctx, &tempF)
		// XXX: update the old f here instead of returning tempF, since
		// `features` were already filled with functions having *f as a receiver.
		// See https://github.com/rclone/rclone/issues/2182
		f.dirCache = tempF.dirCache
		f.root = tempF.root
		// return an error with an fs which points to the parent
		return f, fs.ErrorIsFile
	}
	return f, nil
}

// List the objects and directories in dir into entries.  The
// entries can be returned in any order but should be for a
// complete directory.
//
// dir should be "" to list the root, and should not have
// trailing slashes.
//
// This should return ErrDirNotFound if the directory isn't
// found.
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
	if f.opt.SharedFolder != "" {
		return f.listSharedFiles(ctx, f.opt.SharedFolder)
	}

	dirContent, err := f.listDir(ctx, dir)
	if err != nil {
		return nil, err
	}

	return dirContent, nil
}

// NewObject finds the Object at remote.  If it can't be found
// it returns the error ErrorObjectNotFound.
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
	leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, false)
	if err != nil {
		if err == fs.ErrorDirNotFound {
			return nil, fs.ErrorObjectNotFound
		}
		return nil, err
	}

	folderID, err := strconv.Atoi(directoryID)
	if err != nil {
		return nil, err
	}
	files, err := f.listFiles(ctx, folderID)
	if err != nil {
		return nil, err
	}

	for _, file := range files.Items {
		if file.Filename == leaf {
			path, ok := f.dirCache.GetInv(directoryID)

			if !ok {
				return nil, errors.New("cannot find dir in dircache")
			}

			return f.newObjectFromFile(ctx, path, file), nil
		}
	}

	return nil, fs.ErrorObjectNotFound
}

// Put in to the remote path with the modTime given of the given size
//
// When called from outside an Fs by rclone, src.Size() will always be >= 0.
// But for unknown-sized objects (indicated by src.Size() == -1), Put should either
// return an error or upload it properly (rather than e.g. calling panic).
//
// May create the object even if it returns an error - if so
// will return the object and the error, otherwise will return
// nil and the error
func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
	existingObj, err := f.NewObject(ctx, src.Remote())
	switch err {
	case nil:
		return existingObj, existingObj.Update(ctx, in, src, options...)
	case fs.ErrorObjectNotFound:
		// Not found so create it
		return f.PutUnchecked(ctx, in, src, options...)
	default:
		return nil, err
	}
}

// putUnchecked uploads the object with the given name and size
//
// This will create a duplicate if we upload a new file without
// checking to see if there is one already - use Put() for that.
func (f *Fs) putUnchecked(ctx context.Context, in io.Reader, remote string, size int64, options ...fs.OpenOption) (fs.Object, error) {
	if size > int64(300e9) {
		return nil, errors.New("File too big, can't upload")
	} else if size == 0 {
		return nil, fs.ErrorCantUploadEmptyFiles
	}

	nodeResponse, err := f.getUploadNode(ctx)
	if err != nil {
		return nil, err
	}

	leaf, directoryID, err := f.dirCache.FindPath(ctx, remote, true)
	if err != nil {
		return nil, err
	}

	_, err = f.uploadFile(ctx, in, size, leaf, directoryID, nodeResponse.ID, nodeResponse.URL, options...)
	if err != nil {
		return nil, err
	}

	fileUploadResponse, err := f.endUpload(ctx, nodeResponse.ID, nodeResponse.URL)
	if err != nil {
		return nil, err
	}

	if len(fileUploadResponse.Links) == 0 {
		return nil, errors.New("upload response not found")
	} else if len(fileUploadResponse.Links) > 1 {
		fs.Debugf(remote, "Multiple upload responses found, using the first")
	}

	link := fileUploadResponse.Links[0]
	fileSize, err := strconv.ParseInt(link.Size, 10, 64)

	if err != nil {
		return nil, err
	}

	return &Object{
		fs:     f,
		remote: remote,
		file: File{
			CDN:         0,
			Checksum:    link.Whirlpool,
			ContentType: "",
			Date:        time.Now().Format("2006-01-02 15:04:05"),
			Filename:    link.Filename,
			Pass:        0,
			Size:        fileSize,
			URL:         link.Download,
		},
	}, nil
}

// PutUnchecked uploads the object
//
// This will create a duplicate if we upload a new file without
// checking to see if there is one already - use Put() for that.
func (f *Fs) PutUnchecked(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
	return f.putUnchecked(ctx, in, src.Remote(), src.Size(), options...)
}

// Mkdir makes the directory (container, bucket)
//
// Shouldn't return an error if it already exists
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
	_, err := f.dirCache.FindDir(ctx, dir, true)
	return err
}

// Rmdir removes the directory (container, bucket) if empty
//
// Return an error if it doesn't exist or isn't empty
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
	directoryID, err := f.dirCache.FindDir(ctx, dir, false)
	if err != nil {
		return err
	}

	folderID, err := strconv.Atoi(directoryID)
	if err != nil {
		return err
	}

	_, err = f.removeFolder(ctx, dir, folderID)
	if err != nil {
		return err
	}

	f.dirCache.FlushDir(dir)

	return nil
}

// Move src to this remote using server side move operations.
func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
	srcObj, ok := src.(*Object)
	if !ok {
		fs.Debugf(src, "Can't move - not same remote type")
		return nil, fs.ErrorCantMove
	}
	srcFs := srcObj.fs

	// Find current directory ID
	srcLeaf, srcDirectoryID, err := srcFs.dirCache.FindPath(ctx, srcObj.remote, false)
	if err != nil {
		return nil, err
	}

	// Create temporary object
	dstObj, dstLeaf, dstDirectoryID, err := f.createObject(ctx, remote)
	if err != nil {
		return nil, err
	}

	// If it is in the correct directory, just rename it
	var url string
	if srcDirectoryID == dstDirectoryID {
		// No rename needed
		if srcLeaf == dstLeaf {
			return src, nil
		}
		resp, err := f.renameFile(ctx, srcObj.file.URL, dstLeaf)
		if err != nil {
			return nil, fmt.Errorf("couldn't rename file: %w", err)
		}
		if resp.Status != "OK" {
			return nil, fmt.Errorf("couldn't rename file: %s", resp.Message)
		}
		url = resp.URLs[0].URL
	} else {
		dstFolderID, err := strconv.Atoi(dstDirectoryID)
		if err != nil {
			return nil, err
		}
		rename := dstLeaf
		// No rename needed
		if srcLeaf == dstLeaf {
			rename = ""
		}
		resp, err := f.moveFile(ctx, srcObj.file.URL, dstFolderID, rename)
		if err != nil {
			return nil, fmt.Errorf("couldn't move file: %w", err)
		}
		if resp.Status != "OK" {
			return nil, fmt.Errorf("couldn't move file: %s", resp.Message)
		}
		url = resp.URLs[0]
	}

	file, err := f.readFileInfo(ctx, url)
	if err != nil {
		return nil, errors.New("couldn't read file data")
	}
	dstObj.setMetaData(*file)
	return dstObj, nil
}

// DirMove moves src, srcRemote to this remote at dstRemote
// using server-side move operations.
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantDirMove.
//
// If destination exists then return fs.ErrorDirExists.
//
// This is complicated by the fact that we can't use moveDir to move
// to a different directory AND rename at the same time as it can
// overwrite files in the source directory.
func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
	srcFs, ok := src.(*Fs)
	if !ok {
		fs.Debugf(srcFs, "Can't move directory - not same remote type")
		return fs.ErrorCantDirMove
	}

	srcID, _, _, dstDirectoryID, dstLeaf, err := f.dirCache.DirMove(ctx, srcFs.dirCache, srcFs.root, srcRemote, f.root, dstRemote)
	if err != nil {
		return err
	}
	srcIDnumeric, err := strconv.Atoi(srcID)
	if err != nil {
		return err
	}
	dstDirectoryIDnumeric, err := strconv.Atoi(dstDirectoryID)
	if err != nil {
		return err
	}

	var resp *MoveDirResponse
	resp, err = f.moveDir(ctx, srcIDnumeric, dstLeaf, dstDirectoryIDnumeric)
	if err != nil {
		return fmt.Errorf("couldn't rename leaf: %w", err)
	}
	if resp.Status != "OK" {
		return fmt.Errorf("couldn't rename leaf: %s", resp.Message)
	}

	srcFs.dirCache.FlushDir(srcRemote)
	return nil
}

// Copy src to this remote using server side move operations.
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
	srcObj, ok := src.(*Object)
	if !ok {
		fs.Debugf(src, "Can't move - not same remote type")
		return nil, fs.ErrorCantMove
	}

	// Create temporary object
	dstObj, leaf, directoryID, err := f.createObject(ctx, remote)
	if err != nil {
		return nil, err
	}

	folderID, err := strconv.Atoi(directoryID)
	if err != nil {
		return nil, err
	}
	resp, err := f.copyFile(ctx, srcObj.file.URL, folderID, leaf)
	if err != nil {
		return nil, fmt.Errorf("couldn't move file: %w", err)
	}
	if resp.Status != "OK" {
		return nil, fmt.Errorf("couldn't move file: %s", resp.Message)
	}

	file, err := f.readFileInfo(ctx, resp.URLs[0].ToURL)
	if err != nil {
		return nil, errors.New("couldn't read file data")
	}
	dstObj.setMetaData(*file)
	return dstObj, nil
}

// About gets quota information
func (f *Fs) About(ctx context.Context) (usage *fs.Usage, err error) {
	opts := rest.Opts{
		Method:      "POST",
		Path:        "/user/info.cgi",
		ContentType: "application/json",
	}
	var accountInfo AccountInfo
	var resp *http.Response
	err = f.pacer.Call(func() (bool, error) {
		resp, err = f.rest.CallJSON(ctx, &opts, nil, &accountInfo)
		return shouldRetry(ctx, resp, err)
	})
	if err != nil {
		return nil, fmt.Errorf("failed to read user info: %w", err)
	}

	// FIXME max upload size would be useful to use in Update
	usage = &fs.Usage{
		Used:  fs.NewUsageValue(accountInfo.ColdStorage),                                    // bytes in use
		Total: fs.NewUsageValue(accountInfo.AvailableColdStorage),                           // bytes total
		Free:  fs.NewUsageValue(accountInfo.AvailableColdStorage - accountInfo.ColdStorage), // bytes free
	}
	return usage, nil
}

// PublicLink adds a "readable by anyone with link" permission on the given file or folder.
func (f *Fs) PublicLink(ctx context.Context, remote string, expire fs.Duration, unlink bool) (string, error) {
	o, err := f.NewObject(ctx, remote)
	if err != nil {
		return "", err
	}
	return o.(*Object).file.URL, nil
}

// Check the interfaces are satisfied
var (
	_ fs.Fs              = (*Fs)(nil)
	_ fs.Mover           = (*Fs)(nil)
	_ fs.DirMover        = (*Fs)(nil)
	_ fs.Copier          = (*Fs)(nil)
	_ fs.PublicLinker    = (*Fs)(nil)
	_ fs.PutUncheckeder  = (*Fs)(nil)
	_ dircache.DirCacher = (*Fs)(nil)
)
