package docker

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/rclone/rclone/cmd/mountlib"
	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/config/configmap"
	"github.com/rclone/rclone/fs/fspath"
	"github.com/rclone/rclone/fs/rc"
	"github.com/rclone/rclone/vfs/vfscommon"
	"github.com/rclone/rclone/vfs/vfsflags"

	"github.com/spf13/pflag"
)

// applyOptions configures volume from request options.
//
// There are 5 special options:
//   - "remote" aka "fs" determines existing remote from config file
//     with a path or on-the-fly remote using the ":backend:" syntax.
//     It is usually named "remote" in documentation but can be aliased as
//     "fs" to avoid confusion with the "remote" option of some backends.
//   - "type" is equivalent to the ":backend:" syntax (optional).
//   - "path" provides explicit on-remote path for "type" (optional).
//   - "mount-type" can be "mount", "cmount" or "mount2", defaults to
//     first found (optional).
//   - "persist" is reserved for future to create remotes persisted
//     in rclone.conf similar to rcd (optional).
//
// Unlike rcd we use the flat naming scheme for mount, vfs and backend
// options without substructures. Dashes, underscores and mixed case
// in option names can be used interchangeably. Option name conflicts
// can be resolved in a manner similar to rclone CLI by adding prefixes:
// "vfs-", primary mount backend type like "sftp-", and so on.
//
// After triaging the options are put in MountOpt, VFSOpt or connect
// string for actual filesystem setup and in volume.Options for saving
// the state.
func (vol *Volume) applyOptions(volOpt VolOpts) error {
	// copy options to override later
	mntOpt := &vol.mnt.MountOpt
	vfsOpt := &vol.mnt.VFSOpt
	*mntOpt = vol.drv.mntOpt
	*vfsOpt = vol.drv.vfsOpt

	// vol.Options has all options except "remote" and "type"
	vol.Options = VolOpts{}
	vol.fsString = ""

	var fsName, fsPath, fsType string
	var explicitPath string
	var fsOpt configmap.Simple

	// parse "remote" or "type"
	for key, str := range volOpt {
		switch key {
		case "":
			continue
		case "remote", "fs":
			p, err := fspath.Parse(str)
			if err != nil || p.Name == ":" {
				return fmt.Errorf("cannot parse path %q: %w", str, err)
			}
			fsName, fsPath, fsOpt = p.Name, p.Path, p.Config
			vol.Fs = str
		case "type":
			fsType = str
			vol.Type = str
		case "path":
			explicitPath = str
			vol.Path = str
		default:
			vol.Options[key] = str
		}
	}

	// find options supported by backend
	if strings.HasPrefix(fsName, ":") {
		fsType = fsName[1:]
		fsName = ""
	}
	if fsType == "" {
		fsType = "local"
		if fsName != "" {
			var ok bool
			fsType, ok = fs.ConfigMap(nil, fsName, nil).Get("type")
			if !ok {
				return fs.ErrorNotFoundInConfigFile
			}
		}
	}
	if explicitPath != "" {
		if fsPath != "" {
			fs.Logf(nil, "Explicit path will override connection string")
		}
		fsPath = explicitPath
	}
	fsInfo, err := fs.Find(fsType)
	if err != nil {
		return fmt.Errorf("unknown filesystem type %q", fsType)
	}

	// handle remaining options, override fsOpt
	if fsOpt == nil {
		fsOpt = configmap.Simple{}
	}
	opt := rc.Params{}
	for key, val := range vol.Options {
		opt[key] = val
	}
	for key := range opt {
		var ok bool
		var err error

		switch normalOptName(key) {
		case "persist":
			vol.persist, err = opt.GetBool(key)
			ok = true
		case "mount-type":
			vol.mountType, err = opt.GetString(key)
			ok = true
		}
		if err != nil {
			return fmt.Errorf("cannot parse option %q: %w", key, err)
		}

		if !ok {
			// try to use as a mount option in mntOpt
			ok, err = getMountOption(mntOpt, opt, key)
			if ok && err != nil {
				return fmt.Errorf("cannot parse mount option %q: %w", key, err)
			}
		}
		if !ok {
			// try as a vfs option in vfsOpt
			ok, err = getVFSOption(vfsOpt, opt, key)
			if ok && err != nil {
				return fmt.Errorf("cannot parse vfs option %q: %w", key, err)
			}
		}

		if !ok {
			// try as a backend option in fsOpt (backends use "_" instead of "-")
			optWithPrefix := strings.ReplaceAll(normalOptName(key), "-", "_")
			fsOptName := strings.TrimPrefix(optWithPrefix, fsType+"_")
			hasFsPrefix := optWithPrefix != fsOptName
			if !hasFsPrefix || fsInfo.Options.Get(fsOptName) == nil {
				fs.Logf(nil, "Option %q is not supported by backend %q", key, fsType)
				return fmt.Errorf("unsupported backend option %q", key)
			}
			fsOpt[fsOptName], err = opt.GetString(key)
			if err != nil {
				return fmt.Errorf("cannot parse backend option %q: %w", key, err)
			}
		}
	}

	// build remote string from fsName, fsType, fsOpt, fsPath
	colon := ":"
	comma := ","
	if fsName == "" {
		fsName = ":" + fsType
	}
	connString := fsOpt.String()
	if fsName == "" && fsType == "" {
		colon = ""
		connString = ""
	}
	if connString == "" {
		comma = ""
	}
	vol.fsString = fsName + comma + connString + colon + fsPath

	return vol.validate()
}

func getMountOption(mntOpt *mountlib.Options, opt rc.Params, key string) (ok bool, err error) {
	ok = true
	switch normalOptName(key) {
	case "debug-fuse":
		mntOpt.DebugFUSE, err = opt.GetBool(key)
	case "attr-timeout":
		mntOpt.AttrTimeout, err = opt.GetDuration(key)
	case "option":
		mntOpt.ExtraOptions, err = getStringArray(opt, key)
	case "fuse-flag":
		mntOpt.ExtraFlags, err = getStringArray(opt, key)
	case "daemon":
		mntOpt.Daemon, err = opt.GetBool(key)
	case "daemon-timeout":
		mntOpt.DaemonTimeout, err = opt.GetDuration(key)
	case "default-permissions":
		mntOpt.DefaultPermissions, err = opt.GetBool(key)
	case "allow-non-empty":
		mntOpt.AllowNonEmpty, err = opt.GetBool(key)
	case "allow-root":
		mntOpt.AllowRoot, err = opt.GetBool(key)
	case "allow-other":
		mntOpt.AllowOther, err = opt.GetBool(key)
	case "async-read":
		mntOpt.AsyncRead, err = opt.GetBool(key)
	case "max-read-ahead":
		err = getFVarP(&mntOpt.MaxReadAhead, opt, key)
	case "write-back-cache":
		mntOpt.WritebackCache, err = opt.GetBool(key)
	case "volname":
		mntOpt.VolumeName, err = opt.GetString(key)
	case "noappledouble":
		mntOpt.NoAppleDouble, err = opt.GetBool(key)
	case "noapplexattr":
		mntOpt.NoAppleXattr, err = opt.GetBool(key)
	case "network-mode":
		mntOpt.NetworkMode, err = opt.GetBool(key)
	default:
		ok = false
	}
	return
}

func getVFSOption(vfsOpt *vfscommon.Options, opt rc.Params, key string) (ok bool, err error) {
	var intVal int64
	ok = true
	switch normalOptName(key) {

	// options prefixed with "vfs-"
	case "vfs-cache-mode":
		err = getFVarP(&vfsOpt.CacheMode, opt, key)
	case "vfs-cache-poll-interval":
		vfsOpt.CachePollInterval, err = opt.GetDuration(key)
	case "vfs-cache-max-age":
		vfsOpt.CacheMaxAge, err = opt.GetDuration(key)
	case "vfs-cache-max-size":
		err = getFVarP(&vfsOpt.CacheMaxSize, opt, key)
	case "vfs-read-chunk-size":
		err = getFVarP(&vfsOpt.ChunkSize, opt, key)
	case "vfs-read-chunk-size-limit":
		err = getFVarP(&vfsOpt.ChunkSizeLimit, opt, key)
	case "vfs-case-insensitive":
		vfsOpt.CaseInsensitive, err = opt.GetBool(key)
	case "vfs-write-wait":
		vfsOpt.WriteWait, err = opt.GetDuration(key)
	case "vfs-read-wait":
		vfsOpt.ReadWait, err = opt.GetDuration(key)
	case "vfs-write-back":
		vfsOpt.WriteBack, err = opt.GetDuration(key)
	case "vfs-read-ahead":
		err = getFVarP(&vfsOpt.ReadAhead, opt, key)
	case "vfs-used-is-size":
		vfsOpt.UsedIsSize, err = opt.GetBool(key)

	// unprefixed vfs options
	case "no-modtime":
		vfsOpt.NoModTime, err = opt.GetBool(key)
	case "no-checksum":
		vfsOpt.NoChecksum, err = opt.GetBool(key)
	case "dir-cache-time":
		vfsOpt.DirCacheTime, err = opt.GetDuration(key)
	case "poll-interval":
		vfsOpt.PollInterval, err = opt.GetDuration(key)
	case "read-only":
		vfsOpt.ReadOnly, err = opt.GetBool(key)
	case "dir-perms":
		perms := &vfsflags.FileMode{Mode: &vfsOpt.DirPerms}
		err = getFVarP(perms, opt, key)
	case "file-perms":
		perms := &vfsflags.FileMode{Mode: &vfsOpt.FilePerms}
		err = getFVarP(perms, opt, key)

	// unprefixed unix-only vfs options
	case "umask":
		// GetInt64 doesn't support the `0octal` umask syntax - parse locally
		var strVal string
		if strVal, err = opt.GetString(key); err == nil {
			var longVal int64
			if longVal, err = strconv.ParseInt(strVal, 0, 0); err == nil {
				vfsOpt.Umask = int(longVal)
			}
		}
	case "uid":
		intVal, err = opt.GetInt64(key)
		vfsOpt.UID = uint32(intVal)
	case "gid":
		intVal, err = opt.GetInt64(key)
		vfsOpt.GID = uint32(intVal)

	// non-vfs options
	default:
		ok = false
	}
	return
}

func getFVarP(pvalue pflag.Value, opt rc.Params, key string) error {
	str, err := opt.GetString(key)
	if err != nil {
		return err
	}
	return pvalue.Set(str)
}

func getStringArray(opt rc.Params, key string) ([]string, error) {
	str, err := opt.GetString(key)
	if err != nil {
		return nil, err
	}
	return strings.Split(str, ","), nil
}

func normalOptName(key string) string {
	return strings.ReplaceAll(strings.TrimPrefix(strings.ToLower(key), "--"), "_", "-")
}
