//go:build cmount && windows

package cmount

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"regexp"

	"github.com/rclone/rclone/cmd/mountlib"
	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/lib/file"
)

var isDriveRegex = regexp.MustCompile(`^[a-zA-Z]\:$`)
var isDriveRootPathRegex = regexp.MustCompile(`^[a-zA-Z]\:\\$`)
var isDriveOrRootPathRegex = regexp.MustCompile(`^[a-zA-Z]\:\\?$`)
var isNetworkSharePathRegex = regexp.MustCompile(`^\\\\[^\\\?]+\\[^\\]`)

// isNetworkSharePath returns true if the given string is a valid network share path,
// in the basic UNC format "\\Server\Share\Path", where the first two path components
// are required ("\\Server\Share", which represents the volume).
// Extended-length UNC format "\\?\UNC\Server\Share\Path" is not considered, as it is
// not supported by cgofuse/winfsp, so returns false for any paths with prefix "\\?\".
// Note: There is a UNCPath function in lib/file, but it refers to any extended-length
// paths using prefix "\\?\", and not necessarily network resource UNC paths.
func isNetworkSharePath(l string) bool {
	return isNetworkSharePathRegex.MatchString(l)
}

// isDrive returns true if given string is a drive letter followed by the volume separator, e.g. "X:".
// This is the format supported by cgofuse/winfsp for mounting as drive.
// Extended-length format "\\?\X:" is not considered, as it is not supported by cgofuse/winfsp.
func isDrive(l string) bool {
	return isDriveRegex.MatchString(l)
}

// isDriveRootPath returns true if given string is a drive letter followed by the volume separator,
// as well as a path separator, e.g. "X:\". This is a format often used instead of the format without the
// trailing path separator to denote a drive or volume, in addition to representing the drive's root directory.
// This format is not accepted by cgofuse/winfsp for mounting as drive, but can easily be by trimming off
// the path separator. Extended-length format "\\?\X:\" is not considered.
func isDriveRootPath(l string) bool {
	return isDriveRootPathRegex.MatchString(l)
}

// isDriveOrRootPath returns true if given string is a drive letter followed by the volume separator,
// and optionally a path separator. See isDrive and isDriveRootPath functions.
func isDriveOrRootPath(l string) bool {
	return isDriveOrRootPathRegex.MatchString(l)
}

// isDefaultPath returns true if given string is a special keyword used to trigger default mount.
func isDefaultPath(l string) bool {
	return l == "" || l == "*"
}

// getUnusedDrive find unused drive letter and returns string with drive letter followed by volume separator.
func getUnusedDrive() (string, error) {
	driveLetter := file.FindUnusedDriveLetter()
	if driveLetter == 0 {
		return "", errors.New("could not find unused drive letter")
	}
	mountpoint := string(driveLetter) + ":" // Drive letter with volume separator only, no trailing backslash, which is what cgofuse/winfsp expects
	fs.Logf(nil, "Assigning drive letter %q", mountpoint)
	return mountpoint, nil
}

// handleDefaultMountpath handles the case where mount path is not set, or set to a special keyword.
// This will automatically pick an unused drive letter to use as mountpoint.
func handleDefaultMountpath() (string, error) {
	return getUnusedDrive()
}

// handleNetworkShareMountpath handles the case where mount path is a network share path.
// Sets volume name option and returns a mountpoint string.
func handleNetworkShareMountpath(mountpath string, opt *mountlib.Options) (string, error) {
	// Assuming mount path is a valid network share path (UNC format, "\\Server\Share").
	// Always mount as network drive, regardless of the NetworkMode option.
	// Find an unused drive letter to use as mountpoint, the supplied path can
	// be used as volume prefix (network share path) instead of mountpoint.
	if !opt.NetworkMode {
		fs.Debugf(nil, "Forcing --network-mode because mountpoint path is network share UNC format")
		opt.NetworkMode = true
	}
	mountpoint, err := getUnusedDrive()
	if err != nil {
		return "", err
	}
	return mountpoint, nil
}

// handleLocalMountpath handles the case where mount path is a local file system path.
func handleLocalMountpath(f fs.Fs, mountpath string, opt *mountlib.Options) (string, error) {
	// Assuming path is drive letter or directory path, not network share (UNC) path.
	// If drive letter: Must be given as a single character followed by ":" and nothing else.
	// Else, assume directory path: Directory must not exist, but its parent must.
	if _, err := os.Stat(mountpath); err == nil {
		return "", errors.New("mountpoint path already exists: " + mountpath)
	} else if !os.IsNotExist(err) {
		return "", fmt.Errorf("failed to retrieve mountpoint path information: %w", err)
	}
	if isDriveRootPath(mountpath) { // Assume intention with "X:\" was "X:"
		mountpath = mountpath[:len(mountpath)-1] // WinFsp needs drive mountpoints without trailing path separator
	}
	if !isDrive(mountpath) {
		// Assuming directory path, since it is not a pure drive letter string such as "X:".
		// Drive letter string can be used as is, since we have already checked it does not exist,
		// but directory path needs more checks.
		if opt.NetworkMode {
			fs.Errorf(nil, "Ignoring --network-mode as it is not supported with directory mountpoint")
			opt.NetworkMode = false
		}
		var err error
		if mountpath, err = filepath.Abs(mountpath); err != nil { // Ensures parent is found but also more informative log messages
			return "", fmt.Errorf("mountpoint path is not valid: %s: %w", mountpath, err)
		}
		parent := filepath.Join(mountpath, "..")
		if _, err = os.Stat(parent); err != nil {
			if os.IsNotExist(err) {
				return "", errors.New("parent of mountpoint directory does not exist: " + parent)
			}
			return "", fmt.Errorf("failed to retrieve mountpoint directory parent information: %w", err)
		}
		if err = mountlib.CheckOverlap(f, mountpath); err != nil {
			return "", err
		}
	}
	return mountpath, nil
}

// handleVolumeName handles the volume name option.
func handleVolumeName(opt *mountlib.Options, volumeName string) {
	// If volumeName parameter is set, then just set that into options replacing any existing value.
	// Else, ensure the volume name option is a valid network share UNC path if network mode,
	// and ensure network mode if configured volume name is already UNC path.
	if volumeName != "" {
		opt.VolumeName = volumeName
	} else if opt.VolumeName != "" { // Should always be true due to code in mountlib caller
		// Use value of given volume name option, but check if it is disk volume name or network volume prefix
		if isNetworkSharePath(opt.VolumeName) {
			// Specified volume name is network share UNC path, assume network mode and use it as volume prefix
			opt.VolumeName = opt.VolumeName[1:] // WinFsp requires volume prefix as UNC-like path but with only a single backslash
			if !opt.NetworkMode {
				// Specified volume name is network share UNC path, force network mode and use it as volume prefix
				fs.Debugf(nil, "Forcing network mode due to network share (UNC) volume name")
				opt.NetworkMode = true
			}
		} else if opt.NetworkMode {
			// Plain volume name treated as share name in network mode, append to hard coded "\\server" prefix to get full volume prefix.
			opt.VolumeName = "\\server\\" + opt.VolumeName
		}
	} else if opt.NetworkMode {
		// Hard coded default
		opt.VolumeName = "\\server\\share"
	}
}

// getMountpoint handles mounting details on Windows,
// where disk and network based file systems are treated different.
func getMountpoint(f fs.Fs, mountpath string, opt *mountlib.Options) (mountpoint string, err error) {
	// Inform about some options not relevant in this mode
	if opt.AllowNonEmpty {
		fs.Logf(nil, "--allow-non-empty flag does nothing on Windows")
	}
	if opt.AllowRoot {
		fs.Logf(nil, "--allow-root flag does nothing on Windows")
	}
	if opt.AllowOther {
		fs.Logf(nil, "--allow-other flag does nothing on Windows")
	}

	// Handle mountpath
	var volumeName string
	if isDefaultPath(mountpath) {
		// Mount path indicates defaults, which will automatically pick an unused drive letter.
		mountpoint, err = handleDefaultMountpath()
	} else if isNetworkSharePath(mountpath) {
		// Mount path is a valid network share path (UNC format, "\\Server\Share" prefix).
		mountpoint, err = handleNetworkShareMountpath(mountpath, opt)
		// In this case the volume name is taken from the mount path, will replace any existing volume name option.
		volumeName = mountpath[1:] // WinFsp requires volume prefix as UNC-like path but with only a single backslash
	} else {
		// Mount path is drive letter or directory path.
		mountpoint, err = handleLocalMountpath(f, mountpath, opt)
	}

	// Handle volume name
	handleVolumeName(opt, volumeName)

	// Done, return mountpoint to be used, together with updated mount options.
	if opt.NetworkMode {
		fs.Debugf(nil, "Network mode mounting is enabled")
	} else {
		fs.Debugf(nil, "Network mode mounting is disabled")
	}
	return
}