package folderutil

import (
	"errors"
	"os"
	"path/filepath"
	"runtime"
	"strings"

	fileutil "github.com/projectdiscovery/utils/file"
	mapsutil "github.com/projectdiscovery/utils/maps"
)

var (
	// Separator evaluated at runtime
	Separator = string(os.PathSeparator)
	// Remove source directory after successful sync
	RemoveSourceDirAfterSync = true
	// writeablePathCache is a cache of writeable paths
	writeablePathCache = mapsutil.SyncLockMap[string, struct{}]{
		Map: make(map[string]struct{}),
	}
)

const (
	UnixPathSeparator    = "/"
	WindowsPathSeparator = "\\"
)

// GetFiles within a folder
func GetFiles(root string) ([]string, error) {
	var matches []string
	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}
		matches = append(matches, path)
		return nil
	})
	if err != nil {
		return nil, err
	}
	return matches, nil
}

// PathInfo about a folder
type PathInfo struct {
	IsAbsolute         bool
	RootPath           string
	Parts              []string
	PartsWithSeparator []string
}

// NewPathInfo returns info about a given path
func NewPathInfo(path string) (PathInfo, error) {
	var pathInfo PathInfo
	path = filepath.Clean(path)
	pathInfo.RootPath = filepath.VolumeName(path)
	if filepath.IsAbs(path) {
		if IsUnixOS() {
			if pathInfo.RootPath == "" {
				pathInfo.IsAbsolute = true
				pathInfo.RootPath = UnixPathSeparator
			}
		} else if IsWindowsOS() {
			pathInfo.IsAbsolute = true
			pathInfo.RootPath = pathInfo.RootPath + WindowsPathSeparator
		}
	}

	pathInfo.Parts = agnosticSplit(path)

	for i, pathItem := range pathInfo.Parts {
		if i == 0 && pathInfo.IsAbsolute {
			if IsUnixOS() {
				pathInfo.PartsWithSeparator = append(pathInfo.PartsWithSeparator, pathInfo.RootPath)
			}
		} else if len(pathInfo.PartsWithSeparator) > 0 && pathInfo.PartsWithSeparator[len(pathInfo.PartsWithSeparator)-1] != Separator {
			pathInfo.PartsWithSeparator = append(pathInfo.PartsWithSeparator, Separator)
		}
		pathInfo.PartsWithSeparator = append(pathInfo.PartsWithSeparator, pathItem)
	}
	return pathInfo, nil
}

// Returns all possible combination of the various levels of the path parts
func (pathInfo PathInfo) Paths() ([]string, error) {
	var combos []string
	for i := 0; i <= len(pathInfo.Parts); i++ {
		var computedPath string
		if pathInfo.IsAbsolute && pathInfo.RootPath != "" {
			// on windows we need to skip the volume, already computed in rootpath
			if IsUnixOS() {
				computedPath = pathInfo.RootPath + filepath.Join(pathInfo.Parts[:i]...)
			} else if IsWindowsOS() && i > 0 {
				skipItems := 0
				if len(pathInfo.Parts) > 0 {
					skipItems = 1
				}
				computedPath = pathInfo.RootPath + filepath.Join(pathInfo.Parts[skipItems:i]...)
			}
		} else {
			computedPath = filepath.Join(pathInfo.Parts[:i]...)
		}
		combos = append(combos, filepath.Clean(computedPath))
	}

	return combos, nil
}

// MeshWith combine all values from Path with another provided path
func (pathInfo PathInfo) MeshWith(anotherPath string) ([]string, error) {
	allPaths, err := pathInfo.Paths()
	if err != nil {
		return nil, err
	}
	var combos []string
	for _, basePath := range allPaths {
		combinedPath := filepath.Join(basePath, anotherPath)
		combos = append(combos, filepath.Clean(combinedPath))
	}

	return combos, nil
}

func IsUnixOS() bool {
	switch runtime.GOOS {
	case "android", "darwin", "freebsd", "ios", "linux", "netbsd", "openbsd", "solaris":
		return true
	default:
		return false
	}
}

func IsWindowsOS() bool {
	return runtime.GOOS == "windows"
}

func agnosticSplit(path string) (parts []string) {
	// split with each known separators
	for _, part := range strings.Split(path, UnixPathSeparator) {
		for _, subPart := range strings.Split(part, WindowsPathSeparator) {
			if part != "" {
				parts = append(parts, subPart)
			}
		}
	}
	return
}

// IsWritable checks if a path is writable by attempting to create a temporary file.
// It caches writable paths to avoid unnecessary file operations.
func IsWritable(path string) bool {
	if _, ok := writeablePathCache.Get(path); ok {
		return true
	}
	if !fileutil.FolderExists(path) {
		return false
	}
	tmpfile, err := os.CreateTemp(path, "test")
	if err != nil {
		return false
	}
	_ = tmpfile.Close()
	_ = os.Remove(tmpfile.Name())
	_ = writeablePathCache.Set(path, struct{}{})
	return true
}

// SyncDirectory sync all files and non-empty directories from source to destination folder
// optionally removes source directory and removes source
func SyncDirectory(source, destination string) error {
	// trim trailing slash to avoid slash related issues
	source = strings.TrimSuffix(source, Separator)
	destination = strings.TrimSuffix(destination, Separator)

	if !fileutil.FolderExists(source) {
		return errors.New("source directory doesn't exist")
	}

	if fileutil.FolderExists(destination) {
		sourceStat, err := os.Stat(source)
		if err != nil {
			return err
		}
		destinationStat, err := os.Stat(destination)
		if err != nil {
			return err
		}
		if os.SameFile(sourceStat, destinationStat) {
			return errors.New("source and destination cannot be the same")
		}
	}

	entries, err := os.ReadDir(source)
	if err != nil {
		return err
	}

	for _, entry := range entries {
		sourcePath := filepath.Join(source, entry.Name())
		destPath := filepath.Join(destination, entry.Name())

		if entry.IsDir() {
			subentries, err := os.ReadDir(sourcePath)
			if err != nil {
				return err
			}
			if len(subentries) > 0 {
				err = os.MkdirAll(destPath, os.ModePerm)
				if err != nil {
					return err
				}

				err = SyncDirectory(sourcePath, destPath)
				if err != nil {
					return err
				}
			}
		} else {
			err = os.Rename(sourcePath, destPath)
			if err != nil {
				return err
			}
		}
	}

	if RemoveSourceDirAfterSync {
		err = os.RemoveAll(source)
		if err != nil {
			return err
		}
	}

	return nil
}
