//go:build !plan9 && !solaris

// Package iclouddrive implements the iCloud Drive backend
package iclouddrive

import (
	"bytes"
	"context"
	"path"

	"errors"
	"fmt"
	"io"
	"net/http"
	"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/config/obscure"
	"github.com/rclone/rclone/fs/fserrors"

	"github.com/rclone/rclone/backend/iclouddrive/api"
	"github.com/rclone/rclone/fs/hash"
	"github.com/rclone/rclone/lib/dircache"
	"github.com/rclone/rclone/lib/encoder"
	"github.com/rclone/rclone/lib/pacer"

	"golang.org/x/text/unicode/norm"
)

/*
- dirCache operates on relative path to root
- path sanitization
	- rule of thumb: sanitize before use, but store things as-is
	- the paths cached in dirCache are after sanitizing
	- the remote/dir passed in aren't, and are stored as-is
*/

const (
	configAppleID    = "apple_id"
	configPassword   = "password"
	configClientID   = "client_id"
	configCookies    = "cookies"
	configTrustToken = "trust_token"

	minSleep      = 10 * time.Millisecond
	maxSleep      = 2 * time.Second
	decayConstant = 2
)

// Register with Fs
func init() {
	fs.Register(&fs.RegInfo{
		Name:        "iclouddrive",
		Description: "iCloud Drive",
		Config:      Config,
		NewFs:       NewFs,
		Options: []fs.Option{{
			Name:      configAppleID,
			Help:      "Apple ID.",
			Required:  true,
			Sensitive: true,
		}, {
			Name:       configPassword,
			Help:       "Password.",
			Required:   true,
			IsPassword: true,
			Sensitive:  true,
		}, {
			Name:       configTrustToken,
			Help:       "Trust token (internal use)",
			IsPassword: false,
			Required:   false,
			Sensitive:  true,
			Hide:       fs.OptionHideBoth,
		}, {
			Name:      configCookies,
			Help:      "cookies (internal use only)",
			Required:  false,
			Advanced:  false,
			Sensitive: true,
			Hide:      fs.OptionHideBoth,
		}, {
			Name:     configClientID,
			Help:     "Client id",
			Required: false,
			Advanced: true,
			Default:  "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d",
		}, {
			Name:     config.ConfigEncoding,
			Help:     config.ConfigEncodingHelp,
			Advanced: true,
			Default: (encoder.Display |
				//encoder.EncodeDot |
				encoder.EncodeBackSlash |
				encoder.EncodeInvalidUtf8),
		}},
	})
}

// Options defines the configuration for this backend
type Options struct {
	AppleID    string               `config:"apple_id"`
	Password   string               `config:"password"`
	Photos     bool                 `config:"photos"`
	TrustToken string               `config:"trust_token"`
	Cookies    string               `config:"cookies"`
	ClientID   string               `config:"client_id"`
	Enc        encoder.MultiEncoder `config:"encoding"`
}

// Fs represents a remote icloud drive
type Fs struct {
	name     string // name of this remote
	root     string // the path we are working on.
	rootID   string
	opt      Options            // parsed config options
	features *fs.Features       // optional features
	dirCache *dircache.DirCache // Map of directory path to directory id
	icloud   *api.Client
	service  *api.DriveService
	pacer    *fs.Pacer // pacer for API calls
}

// Object describes an icloud drive object
type Object struct {
	fs          *Fs       // what this object is part of
	remote      string    // The remote path (relative to the fs.root)
	size        int64     // size of the object (on server, after encryption)
	modTime     time.Time // modification time of the object
	createdTime time.Time // creation time of the object
	driveID     string    // item ID of the object
	docID       string    // document ID of the object
	itemID      string    // item ID of the object
	etag        string
	downloadURL string
}

// Config configures the iCloud remote.
func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) {
	var err error
	appleid, _ := m.Get(configAppleID)
	if appleid == "" {
		return nil, errors.New("a apple ID is required")
	}

	password, _ := m.Get(configPassword)
	if password != "" {
		password, err = obscure.Reveal(password)
		if err != nil {
			return nil, err
		}
	}

	trustToken, _ := m.Get(configTrustToken)
	cookieRaw, _ := m.Get(configCookies)
	clientID, _ := m.Get(configClientID)
	cookies := ReadCookies(cookieRaw)

	switch config.State {
	case "":
		icloud, err := api.New(appleid, password, trustToken, clientID, cookies, nil)
		if err != nil {
			return nil, err
		}
		if err := icloud.Authenticate(ctx); err != nil {
			return nil, err
		}
		m.Set(configCookies, icloud.Session.GetCookieString())
		if icloud.Session.Requires2FA() {
			return fs.ConfigInput("2fa_do", "config_2fa", "Two-factor authentication: please enter your 2FA code")
		}
		return nil, nil
	case "2fa_do":
		code := config.Result
		if code == "" {
			return fs.ConfigError("authenticate", "2FA codes can't be blank")
		}

		icloud, err := api.New(appleid, password, trustToken, clientID, cookies, nil)
		if err != nil {
			return nil, err
		}
		if err := icloud.SignIn(ctx); err != nil {
			return nil, err
		}

		if err := icloud.Session.Validate2FACode(ctx, code); err != nil {
			return nil, err
		}

		m.Set(configTrustToken, icloud.Session.TrustToken)
		m.Set(configCookies, icloud.Session.GetCookieString())
		return nil, nil

	case "2fa_error":
		if config.Result == "true" {
			return fs.ConfigGoto("2fa")
		}
		return nil, errors.New("2fa authentication failed")
	}
	return nil, fmt.Errorf("unknown state %q", config.State)
}

// find item by path. Will not return any children for the item
func (f *Fs) findItem(ctx context.Context, dir string) (item *api.DriveItem, found bool, err error) {
	var resp *http.Response
	if err = f.pacer.Call(func() (bool, error) {
		item, resp, err = f.service.GetItemByPath(ctx, path.Join(f.root, dir))
		return shouldRetry(ctx, resp, err)
	}); err != nil {
		if item == nil && resp.StatusCode == 404 {
			return nil, false, nil
		}
		return nil, false, err
	}

	return item, true, nil
}

func (f *Fs) findLeafItem(ctx context.Context, pathID string, leaf string) (item *api.DriveItem, found bool, err error) {
	items, err := f.listAll(ctx, pathID)
	if err != nil {
		return nil, false, err
	}
	for _, item := range items {
		// iCloud returns file names in NFD Unicode normalization, so normalized to NFC for consistent comparison
		if strings.EqualFold(norm.NFC.String(item.FullName()), leaf) {
			return item, true, nil
		}
	}

	return nil, false, nil

}

// FindLeaf finds a directory of name leaf in the folder with ID pathID
func (f *Fs) FindLeaf(ctx context.Context, pathID string, leaf string) (pathIDOut string, found bool, err error) {
	item, found, err := f.findLeafItem(ctx, pathID, leaf)

	if err != nil {
		return "", found, err
	}

	if !found {
		return "", false, err
	}

	if !item.IsFolder() {
		return "", false, fs.ErrorIsFile
	}

	return f.IDJoin(item.Drivewsid, item.Etag), true, nil
}

// Features implements fs.Fs.
func (f *Fs) Features() *fs.Features {
	return f.features
}

// Hashes are not exposed anywhere
func (f *Fs) Hashes() hash.Set {
	return hash.Set(hash.None)
}

func (f *Fs) purgeCheck(ctx context.Context, dir string, check bool) error {
	root := path.Join(f.root, dir)
	if root == "" {
		return errors.New("can't purge root directory")
	}

	directoryID, etag, err := f.FindDir(ctx, dir, false)
	if err != nil {
		return err
	}

	if check {
		item, found, err := f.findItem(ctx, dir)
		if err != nil {
			return err
		}

		if found && item.DirectChildrenCount > 0 {
			return fs.ErrorDirectoryNotEmpty
		}
	}

	var _ *api.DriveItem
	var resp *http.Response
	if err = f.pacer.Call(func() (bool, error) {
		_, resp, err = f.service.MoveItemToTrashByID(ctx, directoryID, etag, true)
		return retryResultUnknown(ctx, resp, err)
	}); err != nil {
		return err
	}

	// flush everything from the left of the dir
	f.dirCache.FlushDir(dir)

	return nil
}

// Purge all files in the directory specified
//
// Implement this if you have a way of deleting all the files
// quicker than just running Remove() on the result of List()
//
// Return an error if it doesn't exist
func (f *Fs) Purge(ctx context.Context, dir string) error {
	if dir == "" {
		return fs.ErrorCantPurge
	}
	return f.purgeCheck(ctx, dir, false)
}

func (f *Fs) listAll(ctx context.Context, dirID string) (items []*api.DriveItem, err error) {
	var item *api.DriveItem
	var resp *http.Response

	if err = f.pacer.Call(func() (bool, error) {
		id, _ := f.parseNormalizedID(dirID)
		item, resp, err = f.service.GetItemByDriveID(ctx, id, true)
		return shouldRetry(ctx, resp, err)
	}); err != nil {
		return nil, err
	}

	items = item.Items

	for i, item := range items {
		item.Name = f.opt.Enc.ToStandardName(item.Name)
		item.Extension = f.opt.Enc.ToStandardName(item.Extension)
		items[i] = item
	}

	return items, nil
}

// List implements fs.Fs.
func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
	dirRemoteID, err := f.dirCache.FindDir(ctx, dir, false)
	if err != nil {
		return nil, err
	}

	entries = make(fs.DirEntries, 0)
	items, err := f.listAll(ctx, dirRemoteID)

	if err != nil {
		return nil, err
	}

	for _, item := range items {
		id := item.Drivewsid
		name := item.FullName()
		remote := path.Join(dir, name)
		if item.IsFolder() {
			jid := f.putFolderCache(id, item.Etag, remote)
			d := fs.NewDir(remote, item.DateModified).SetID(jid).SetSize(item.AssetQuota)
			entries = append(entries, d)
		} else {
			o, err := f.NewObjectFromDriveItem(ctx, remote, item)
			if err != nil {
				return nil, err
			}
			entries = append(entries, o)
		}
	}

	return entries, nil
}

// Mkdir implements fs.Fs.
func (f *Fs) Mkdir(ctx context.Context, dir string) error {
	_, _, err := f.FindDir(ctx, dir, true)
	return err
}

// Name implements fs.Fs.
func (f *Fs) Name() string {
	return f.name
}

// Precision implements fs.Fs.
func (f *Fs) Precision() time.Duration {
	return time.Second
}

// Copy src to this remote using server-side copy operations.
//
// This is stored with the remote path given.
//
// It returns the destination Object and a possible error.
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantCopy
//
//nolint:all
func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
	// ICloud cooy endpoint is broken. Once they fixed it this can be re-enabled.
	return nil, fs.ErrorCantCopy

	// note: so many calls its only just faster then a reupload for big files.
	srcObj, ok := src.(*Object)
	if !ok {
		fs.Debugf(src, "Can't copy - not same remote type")
		return nil, fs.ErrorCantCopy
	}

	file, pathID, _, err := f.FindPath(ctx, remote, true)
	if err != nil {
		return nil, err
	}

	var resp *http.Response
	var info *api.DriveItemRaw

	// make a copy
	if err = f.pacer.Call(func() (bool, error) {
		info, resp, err = f.service.CopyDocByItemID(ctx, srcObj.itemID)
		return retryResultUnknown(ctx, resp, err)
	}); err != nil {
		return nil, err
	}

	// renaming in CopyDocByID endpoint does not work :/ so do it the hard way

	// get new document
	var doc *api.Document
	if err = f.pacer.Call(func() (bool, error) {
		doc, resp, err = f.service.GetDocByItemID(ctx, info.ItemID)
		return shouldRetry(ctx, resp, err)
	}); err != nil {
		return nil, err
	}

	// get parentdrive id
	var dirDoc *api.Document
	if err = f.pacer.Call(func() (bool, error) {
		dirDoc, resp, err = f.service.GetDocByItemID(ctx, pathID)
		return shouldRetry(ctx, resp, err)
	}); err != nil {
		return nil, err
	}

	// build request
	// can't use normal rename as file needs to be "activated" first

	r := api.NewUpdateFileInfo()
	r.DocumentID = doc.DocumentID
	r.Path.Path = file
	r.Path.StartingDocumentID = dirDoc.DocumentID
	r.Data.Signature = doc.Data.Signature
	r.Data.ReferenceSignature = doc.Data.ReferenceSignature
	r.Data.WrappingKey = doc.Data.WrappingKey
	r.Data.Size = doc.Data.Size
	r.Mtime = srcObj.modTime.UnixMilli()
	r.Btime = srcObj.modTime.UnixMilli()

	var item *api.DriveItem
	if err = f.pacer.Call(func() (bool, error) {
		item, resp, err = f.service.UpdateFile(ctx, &r)
		return retryResultUnknown(ctx, resp, err)
	}); err != nil {
		return nil, err
	}

	o, err := f.NewObjectFromDriveItem(ctx, remote, item)
	if err != nil {
		return nil, err
	}
	obj := o.(*Object)

	// cheat unit tests
	obj.modTime = srcObj.modTime
	obj.createdTime = srcObj.createdTime

	return obj, nil
}

// 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) {
	size := src.Size()
	if size < 0 {
		return nil, errors.New("file size unknown")
	}
	existingObj, err := f.NewObject(ctx, src.Remote())
	switch err {
	case nil:
		// object is found
		return existingObj, existingObj.Update(ctx, in, src, options...)
	case fs.ErrorObjectNotFound:
		// object not found, so we need to create it
		remote := src.Remote()
		size := src.Size()
		modTime := src.ModTime(ctx)

		obj, err := f.createObject(ctx, remote, modTime, size)
		if err != nil {
			return nil, err
		}
		return obj, obj.Update(ctx, in, src, options...)
	default:
		// real error caught
		return nil, err
	}
}

// DirCacheFlush resets the directory cache - used in testing as an
// optional interface
func (f *Fs) DirCacheFlush() {
	f.dirCache.ResetRoot()
}

// parseNormalizedID parses a normalized ID (may be in the form `driveID#itemID` or just `itemID`)
// and returns itemID, driveID, rootURL.
// Such a normalized ID can come from (*Item).GetID()
//
// Parameters:
// - rid: the normalized ID to be parsed
//
// Returns:
// - id: the itemID extracted from the normalized ID
// - etag: the driveID extracted from the normalized ID, or an empty string if not present
func (f *Fs) parseNormalizedID(rid string) (id string, etag string) {
	split := strings.Split(rid, "#")
	if len(split) == 1 {
		return split[0], ""
	}
	return split[0], split[1]
}

// FindPath finds the leaf and directoryID from a normalized path
func (f *Fs) FindPath(ctx context.Context, remote string, create bool) (leaf, directoryID, etag string, err error) {
	leaf, jDirectoryID, err := f.dirCache.FindPath(ctx, remote, create)
	if err != nil {
		return "", "", "", err
	}
	directoryID, etag = f.parseNormalizedID(jDirectoryID)
	return leaf, directoryID, etag, nil
}

// FindDir finds the directory passed in returning the directory ID
// starting from pathID
func (f *Fs) FindDir(ctx context.Context, path string, create bool) (pathID string, etag string, err error) {
	jDirectoryID, err := f.dirCache.FindDir(ctx, path, create)
	if err != nil {
		return "", "", err
	}
	directoryID, etag := f.parseNormalizedID(jDirectoryID)
	return directoryID, etag, nil
}

// IDJoin joins the given ID and ETag into a single string with a "#" delimiter.
func (f *Fs) IDJoin(id string, etag string) string {
	if strings.Contains(id, "#") {
		// already contains an etag, replace
		id, _ = f.parseNormalizedID(id)
	}

	return strings.Join([]string{id, etag}, "#")
}

func (f *Fs) putFolderCache(id, etag, remote string) string {
	jid := f.IDJoin(id, etag)
	f.dirCache.Put(remote, f.IDJoin(id, etag))
	return jid
}

// Rmdir implements fs.Fs.
func (f *Fs) Rmdir(ctx context.Context, dir string) error {
	return f.purgeCheck(ctx, dir, true)
}

// Root implements fs.Fs.
func (f *Fs) Root() string {
	return f.opt.Enc.ToStandardPath(f.root)
}

// String implements fs.Fs.
func (f *Fs) String() string {
	return f.root
}

// CreateDir makes a directory with pathID as parent and name leaf
//
// This should be implemented by the backend and will be called by the
// dircache package when appropriate.
func (f *Fs) CreateDir(ctx context.Context, pathID, leaf string) (string, error) {
	var item *api.DriveItem
	var err error
	var found bool
	var resp *http.Response
	if err = f.pacer.Call(func() (bool, error) {
		id, _ := f.parseNormalizedID(pathID)
		item, resp, err = f.service.CreateNewFolderByDriveID(ctx, id, f.opt.Enc.FromStandardName(leaf))

		// check if it went oke
		if requestError, ok := err.(*api.RequestError); ok {
			if requestError.Status == "unknown" {
				fs.Debugf(requestError, " checking if dir is created with separate call.")
				time.Sleep(1 * time.Second) // sleep to give icloud time to clear up its mind
				item, found, err = f.findLeafItem(ctx, pathID, leaf)
				if err != nil {
					return false, err
				}

				if !found {
					// lets assume it failed and retry
					return true, err
				}

				// success, clear err
				err = nil
			}
		}

		return ignoreResultUnknown(ctx, resp, err)
	}); err != nil {
		return "", err
	}

	return f.IDJoin(item.Drivewsid, item.Etag), err
}

// 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
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, jsrcDirectoryID, srcLeaf, jdstDirectoryID, dstLeaf, err := f.dirCache.DirMove(ctx, srcFs.dirCache, srcFs.root, srcRemote, f.root, dstRemote)
	if err != nil {
		return err
	}

	srcDirectoryID, srcEtag := f.parseNormalizedID(jsrcDirectoryID)
	dstDirectoryID, _ := f.parseNormalizedID(jdstDirectoryID)

	_, err = f.move(ctx, srcID, srcDirectoryID, srcLeaf, srcEtag, dstDirectoryID, dstLeaf)
	if err != nil {
		return err
	}

	srcFs.dirCache.FlushDir(srcRemote)

	return nil
}

func (f *Fs) move(ctx context.Context, ID, srcDirectoryID, srcLeaf, srcEtag, dstDirectoryID, dstLeaf string) (*api.DriveItem, error) {
	var resp *http.Response
	var item *api.DriveItem
	var err error

	// move
	if srcDirectoryID != dstDirectoryID {
		if err = f.pacer.Call(func() (bool, error) {
			id, _ := f.parseNormalizedID(ID)
			item, resp, err = f.service.MoveItemByDriveID(ctx, id, srcEtag, dstDirectoryID, true)
			return ignoreResultUnknown(ctx, resp, err)
		}); err != nil {
			return nil, err
		}
		ID = item.Drivewsid
		srcEtag = item.Etag
	}

	// rename
	if srcLeaf != dstLeaf {
		if err = f.pacer.Call(func() (bool, error) {
			id, _ := f.parseNormalizedID(ID)
			item, resp, err = f.service.RenameItemByDriveID(ctx, id, srcEtag, dstLeaf, true)
			return ignoreResultUnknown(ctx, resp, err)
		}); err != nil {
			return item, err
		}
	}

	return item, err
}

// Move moves the src object to the specified remote.
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
	}

	srcLeaf, srcDirectoryID, _, err := srcObj.fs.FindPath(ctx, srcObj.remote, true)
	if err != nil {
		return nil, err
	}

	dstLeaf, dstDirectoryID, _, err := f.FindPath(ctx, remote, true)
	if err != nil {
		return nil, err
	}

	item, err := f.move(ctx, srcObj.driveID, srcDirectoryID, srcLeaf, srcObj.etag, dstDirectoryID, dstLeaf)
	if err != nil {
		return src, err
	}

	return f.NewObjectFromDriveItem(ctx, remote, item)
}

// Creates from the parameters passed in a half finished Object which
// must have setMetaData called on it
//
// Returns the object, leaf, directoryID and error.
//
// Used to create new objects
func (f *Fs) createObject(ctx context.Context, remote string, modTime time.Time, size int64) (o *Object, err error) {
	// Create the directory for the object if it doesn't exist
	_, _, _, err = f.FindPath(ctx, remote, true)
	if err != nil {
		return
	}
	// Temporary Object under construction
	o = &Object{
		fs:      f,
		remote:  remote,
		modTime: modTime,
		size:    size,
	}
	return o, nil
}

// ReadCookies parses the raw cookie string and returns an array of http.Cookie objects.
func ReadCookies(raw string) []*http.Cookie {
	header := http.Header{}
	header.Add("Cookie", raw)
	request := http.Request{Header: header}
	return request.Cookies()
}

var retryErrorCodes = []int{
	400, // icloud is a mess, sometimes returns 400 on a perfectly fine request. So just retry
	408, // Request Timeout
	409, // Conflict, retry could fix it.
	429, // Rate exceeded.
	500, // Get occasional 500 Internal Server Error
	502, // Server overload
	503, // Service Unavailable
	504, // Gateway Time-out
}

func shouldRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
	if fserrors.ContextError(ctx, &err) {
		return false, err
	}

	return fserrors.ShouldRetry(err) || fserrors.ShouldRetryHTTP(resp, retryErrorCodes), err
}

func ignoreResultUnknown(ctx context.Context, resp *http.Response, err error) (bool, error) {
	if requestError, ok := err.(*api.RequestError); ok {
		if requestError.Status == "unknown" {
			fs.Debugf(requestError, " ignoring.")
			return false, nil
		}
	}
	return shouldRetry(ctx, resp, err)
}

func retryResultUnknown(ctx context.Context, resp *http.Response, err error) (bool, error) {
	if requestError, ok := err.(*api.RequestError); ok {
		if requestError.Status == "unknown" {
			fs.Debugf(requestError, " retrying.")
			return true, err
		}
	}
	return shouldRetry(ctx, resp, err)
}

// NewFs constructs an Fs from the path, container:path
func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
	// Parse config into Options struct
	opt := new(Options)
	err := configstruct.Set(m, opt)
	if err != nil {
		return nil, err
	}

	if opt.Password != "" {
		var err error
		opt.Password, err = obscure.Reveal(opt.Password)
		if err != nil {
			return nil, fmt.Errorf("couldn't decrypt user password: %w", err)
		}
	}

	if opt.TrustToken == "" {
		return nil, fmt.Errorf("missing icloud trust token: try refreshing it with \"rclone config reconnect %s:\"", name)
	}

	cookies := ReadCookies(opt.Cookies)

	callback := func(session *api.Session) {
		m.Set(configCookies, session.GetCookieString())
	}

	icloud, err := api.New(
		opt.AppleID,
		opt.Password,
		opt.TrustToken,
		opt.ClientID,
		cookies,
		callback,
	)
	if err != nil {
		return nil, err
	}

	if err := icloud.Authenticate(ctx); err != nil {
		return nil, err
	}

	if icloud.Session.Requires2FA() {
		return nil, errors.New("trust token expired, please reauth")
	}

	root = strings.Trim(root, "/")

	f := &Fs{
		name:   name,
		root:   root,
		icloud: icloud,
		rootID: "FOLDER::com.apple.CloudDocs::root",
		opt:    *opt,
		pacer:  fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
	}
	f.features = (&fs.Features{
		CanHaveEmptyDirectories: true,
		PartialUploads:          false,
	}).Fill(ctx, f)

	rootID := f.rootID
	f.service, err = icloud.DriveService()
	if err != nil {
		return nil, err
	}

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

	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.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
}

// NewObject creates a new fs.Object from a given remote string.
//
// ctx: The context.Context for the function.
// remote: The remote string representing the object's location.
// Returns an fs.Object and an error.
func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
	return f.NewObjectFromDriveItem(ctx, remote, nil)
}

// NewObjectFromDriveItem creates a new fs.Object from a given remote string and DriveItem.
//
// ctx: The context.Context for the function.
// remote: The remote string representing the object's location.
// item: The optional DriveItem to use for initializing the Object. If nil, the function will read the metadata from the remote location.
// Returns an fs.Object and an error.
func (f *Fs) NewObjectFromDriveItem(ctx context.Context, remote string, item *api.DriveItem) (fs.Object, error) {
	o := &Object{
		fs:     f,
		remote: remote,
	}
	if item != nil {
		err := o.setMetaData(item)
		if err != nil {
			return nil, err
		}
	} else {
		item, err := f.readMetaData(ctx, remote)

		if err != nil {
			return nil, err
		}

		err = o.setMetaData(item)
		if err != nil {
			return nil, err
		}
	}

	return o, nil
}

func (f *Fs) readMetaData(ctx context.Context, path string) (item *api.DriveItem, err error) {
	leaf, ID, _, err := f.FindPath(ctx, path, false)

	if err != nil {
		if err == fs.ErrorDirNotFound {
			return nil, fs.ErrorObjectNotFound
		}
		return nil, err
	}

	item, found, err := f.findLeafItem(ctx, ID, leaf)

	if err != nil {
		return nil, err
	}

	if !found {
		return nil, fs.ErrorObjectNotFound
	}

	return item, nil
}

func (o *Object) setMetaData(item *api.DriveItem) (err error) {
	if item.IsFolder() {
		return fs.ErrorIsDir
	}
	o.size = item.Size
	o.modTime = item.DateModified
	o.createdTime = item.DateCreated
	o.driveID = item.Drivewsid
	o.docID = item.Docwsid
	o.itemID = item.Itemid
	o.etag = item.Etag
	o.downloadURL = item.DownloadURL()
	return nil
}

// ID returns the ID of the Object if known, or "" if not
func (o *Object) ID() string {
	return o.driveID
}

// Fs implements fs.Object.
func (o *Object) Fs() fs.Info {
	return o.fs
}

// Hash implements fs.Object.
func (o *Object) Hash(ctx context.Context, t hash.Type) (string, error) {
	return "", hash.ErrUnsupported
}

// ModTime implements fs.Object.
func (o *Object) ModTime(context.Context) time.Time {
	return o.modTime
}

// Open implements fs.Object.
func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) {
	fs.FixRangeOption(options, o.size)

	// Drive does not support empty files, so we cheat
	if o.size == 0 {
		return io.NopCloser(bytes.NewBufferString("")), nil
	}

	var resp *http.Response
	var err error

	if err = o.fs.pacer.Call(func() (bool, error) {
		var url string

		//var doc *api.Document
		//if o.docID == "" {
		//doc, resp, err = o.fs.service.GetDocByItemID(ctx, o.itemID)
		//}

		// Can not get the download url on a item to work, so do it the hard way.
		url, _, err = o.fs.service.GetDownloadURLByDriveID(ctx, o.driveID)

		resp, err = o.fs.service.DownloadFile(ctx, url, options)
		return shouldRetry(ctx, resp, err)
	}); err != nil {
		return nil, err
	}

	return resp.Body, err
}

// Remote implements fs.Object.
func (o *Object) Remote() string {
	return o.remote
}

// Remove implements fs.Object.
func (o *Object) Remove(ctx context.Context) error {
	if o.itemID == "" {
		return nil
	}

	var resp *http.Response
	var err error
	if err = o.fs.pacer.Call(func() (bool, error) {
		_, resp, err = o.fs.service.MoveItemToTrashByID(ctx, o.driveID, o.etag, true)
		return retryResultUnknown(ctx, resp, err)
	}); err != nil {
		return err
	}

	return nil
}

// SetModTime implements fs.Object.
func (o *Object) SetModTime(ctx context.Context, t time.Time) error {
	return fs.ErrorCantSetModTime
}

// Size implements fs.Object.
func (o *Object) Size() int64 {
	return o.size
}

// Storable implements fs.Object.
func (o *Object) Storable() bool {
	return true
}

// String implements fs.Object.
func (o *Object) String() string {
	if o == nil {
		return "<nil>"
	}
	return o.remote
}

// Update implements fs.Object.
func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
	size := src.Size()
	if size < 0 {
		return errors.New("file size unknown")
	}

	remote := o.Remote()
	modTime := src.ModTime(ctx)

	leaf, dirID, _, err := o.fs.FindPath(ctx, path.Clean(remote), true)
	if err != nil {
		return err
	}

	// Move current file to trash
	if o.driveID != "" {
		err = o.Remove(ctx)
		if err != nil {
			return err
		}
	}

	name := o.fs.opt.Enc.FromStandardName(leaf)
	var resp *http.Response

	// Create document
	var uploadInfo *api.UploadResponse
	if err = o.fs.pacer.Call(func() (bool, error) {
		uploadInfo, resp, err = o.fs.service.CreateUpload(ctx, size, name)
		return ignoreResultUnknown(ctx, resp, err)
	}); err != nil {
		return err
	}

	// Upload content
	var upload *api.SingleFileResponse
	if err = o.fs.pacer.Call(func() (bool, error) {
		upload, resp, err = o.fs.service.Upload(ctx, in, size, name, uploadInfo.URL)
		return ignoreResultUnknown(ctx, resp, err)
	}); err != nil {
		return err
	}

	//var doc *api.Document
	//if err = o.fs.pacer.Call(func() (bool, error) {
	//	doc, resp, err = o.fs.service.GetDocByItemID(ctx, dirID)
	//	return ignoreResultUnknown(ctx, resp, err)
	//}); err != nil {
	//	return err
	//}

	r := api.NewUpdateFileInfo()
	r.DocumentID = uploadInfo.DocumentID
	r.Path.Path = name
	r.Path.StartingDocumentID = api.GetDocIDFromDriveID(dirID)
	//r.Path.StartingDocumentID = doc.DocumentID
	r.Data.Receipt = upload.SingleFile.Receipt
	r.Data.Signature = upload.SingleFile.Signature
	r.Data.ReferenceSignature = upload.SingleFile.ReferenceSignature
	r.Data.WrappingKey = upload.SingleFile.WrappingKey
	r.Data.Size = upload.SingleFile.Size
	r.Mtime = modTime.Unix() * 1000
	r.Btime = modTime.Unix() * 1000

	// Update metadata
	var item *api.DriveItem
	if err = o.fs.pacer.Call(func() (bool, error) {
		item, resp, err = o.fs.service.UpdateFile(ctx, &r)
		return ignoreResultUnknown(ctx, resp, err)
	}); err != nil {
		return err
	}

	err = o.setMetaData(item)
	if err != nil {
		return err
	}

	o.modTime = modTime
	o.size = src.Size()

	return nil
}

// Check interfaces are satisfied
var (
	_ fs.Fs              = &Fs{}
	_ fs.Mover           = (*Fs)(nil)
	_ fs.Purger          = (*Fs)(nil)
	_ fs.DirMover        = (*Fs)(nil)
	_ fs.DirCacheFlusher = (*Fs)(nil)
	_ fs.Copier          = (*Fs)(nil)
	_ fs.Object          = &Object{}
	_ fs.IDer            = (*Object)(nil)
)
