package fio

import (
	"log"
	"math/rand"
	"os"
	"path/filepath"

	"github.com/pkg/errors"
)

// WriteFiles writes files to the directory specified by path, up to the
// provided size and number of files.
func (fr *Runner) WriteFiles(relPath string, opt Options) error {
	lock, err := fr.PathLock.Lock(relPath)
	if err != nil {
		return err
	}
	defer lock.Unlock()

	fullPath := filepath.Join(fr.LocalDataDir, relPath)

	return fr.writeFiles(fullPath, opt)
}

func (fr *Runner) writeFiles(fullPath string, opt Options) error {
	err := os.MkdirAll(fullPath, 0o700)
	if err != nil {
		return errors.Wrap(err, "unable to make directory for write")
	}

	relWritePath, err := filepath.Rel(fr.LocalDataDir, fullPath)
	if err != nil {
		return errors.Wrapf(err, "error finding relative file path between %v and %v", fr.LocalDataDir, fullPath)
	}

	absWritePath := filepath.Join(fr.FioWriteBaseDir, relWritePath)

	_, _, err = fr.RunConfigs(Config{
		{
			Name: "writeFiles",
			Options: opt.Merge(
				Options{
					"readwrite":       RandWriteFio,
					"filename_format": "file_$filenum",
				}.WithDirectory(absWritePath),
			),
		},
	})

	return err
}

// WriteFilesAtDepth writes files to a directory "depth" layers deep below
// the base data directory.
func (fr *Runner) WriteFilesAtDepth(relBasePath string, depth int, opt Options) error {
	lock, err := fr.PathLock.Lock(relBasePath)
	if err != nil {
		return err
	}
	defer lock.Unlock()

	fullBasePath := filepath.Join(fr.LocalDataDir, relBasePath)

	err = os.MkdirAll(fullBasePath, 0o700)
	if err != nil {
		return errors.Wrapf(err, "unable to make base dir %v for writing at depth", fullBasePath)
	}

	return fr.writeFilesAtDepth(fullBasePath, depth, depth, opt)
}

// WriteFilesAtDepthRandomBranch writes files to a directory "depth" layers deep below
// the base data directory and branches at a random depth.
func (fr *Runner) WriteFilesAtDepthRandomBranch(relBasePath string, depth int, opt Options) error {
	lock, err := fr.PathLock.Lock(relBasePath)
	if err != nil {
		return err
	}
	defer lock.Unlock()

	fullBasePath := filepath.Join(fr.LocalDataDir, relBasePath)

	err = os.MkdirAll(fullBasePath, 0o700)
	if err != nil {
		return errors.Wrapf(err, "unable to make base dir %v for writing at depth with a branch", fullBasePath)
	}

	return fr.writeFilesAtDepth(fullBasePath, depth, rand.Intn(depth+1), opt)
}

// DeleteRelDir deletes a relative directory in the runner's data directory.
func (fr *Runner) DeleteRelDir(relDirPath string) error {
	lock, err := fr.PathLock.Lock(relDirPath)
	if err != nil {
		return err
	}
	defer lock.Unlock()

	return os.RemoveAll(filepath.Join(fr.LocalDataDir, relDirPath))
}

// DeleteDirAtDepth deletes a random directory at the given depth.
func (fr *Runner) DeleteDirAtDepth(relBasePath string, depth int) error {
	lock, err := fr.PathLock.Lock(relBasePath)
	if err != nil {
		return err
	}
	defer lock.Unlock()

	if depth == 0 {
		return ErrCanNotDeleteRoot
	}

	fullBasePath := filepath.Join(fr.LocalDataDir, relBasePath)

	return fr.operateAtDepth(fullBasePath, depth, os.RemoveAll)
}

// DeleteContentsAtDepth deletes some or all of the contents of a directory
// at the provided depths. The probability argument denotes the probability in the
// range [0.0,1.0] that a given file system entry in the directory at this depth will be
// deleted. Probability set to 0 will delete nothing. Probability set to 1 will delete
// everything.
func (fr *Runner) DeleteContentsAtDepth(relBasePath string, depth int, prob float32) error {
	lock, err := fr.PathLock.Lock(relBasePath)
	if err != nil {
		return err
	}
	defer lock.Unlock()

	fullBasePath := filepath.Join(fr.LocalDataDir, relBasePath)

	return fr.operateAtDepth(fullBasePath, depth, func(dirPath string) error {
		dirEntries, err := os.ReadDir(dirPath)
		if err != nil {
			return err
		}

		for _, entry := range dirEntries {
			if rand.Float32() < prob {
				path := filepath.Join(dirPath, entry.Name())
				err = os.RemoveAll(path)
				if err != nil {
					return err
				}
			}
		}

		return nil
	})
}

// List of known errors.
var (
	ErrNoDirFound       = errors.New("no directory found at this depth")
	ErrCanNotDeleteRoot = errors.New("can not delete root directory")
)

func (fr *Runner) operateAtDepth(path string, depth int, f func(string) error) error {
	if depth <= 0 {
		log.Printf("performing operation on directory %s\n", path)
		return f(path)
	}

	dirEntries, err := os.ReadDir(path)
	if err != nil {
		return errors.Wrapf(err, "unable to read dir at path %v", path)
	}

	var dirList []string

	for _, entry := range dirEntries {
		if entry.IsDir() {
			dirList = append(dirList, filepath.Join(path, entry.Name()))
		}
	}

	rand.Shuffle(len(dirList), func(i, j int) {
		dirList[i], dirList[j] = dirList[j], dirList[i]
	})

	for _, dirName := range dirList {
		err = fr.operateAtDepth(dirName, depth-1, f)
		if !errors.Is(err, ErrNoDirFound) {
			return err
		}
	}

	return ErrNoDirFound
}

// writeFilesAtDepth traverses the file system tree until it reaches a given "depth", at which
// point it uses fio to write one or more files controlled by the provided Options.
// The "branchDepth" argument gives control over whether the files will be written into
// existing directories or a new path entirely. The function will traverse existing directories
// (if any) until it reaches "branchDepth", after which it will begin creating new directories
// for the remainder of the path until "depth" is reached.
// If "branchDepth" is zero, the entire path will be newly created directories.
// If "branchDepth" is greater than or equal to "depth", the files will be written to
// a random existing directory, if one exists at that depth.
func (fr *Runner) writeFilesAtDepth(fromDirPath string, depth, branchDepth int, opt Options) error {
	if depth <= 0 {
		return fr.writeFiles(fromDirPath, opt)
	}

	var subdirPath string

	if branchDepth > 0 {
		subdirPath = pickRandSubdirPath(fromDirPath)
	}

	if subdirPath == "" {
		var err error

		// Couldn't find a subdir, create one instead
		subdirPath, err = os.MkdirTemp(fromDirPath, "dir_")
		if err != nil {
			return errors.Wrapf(err, "unable to create temp dir at %v", fromDirPath)
		}
	}

	return fr.writeFilesAtDepth(subdirPath, depth-1, branchDepth-1, opt)
}

func pickRandSubdirPath(dirPath string) (subdirPath string) {
	dirEntries, err := os.ReadDir(dirPath)
	if err != nil {
		return ""
	}

	// Find all entries that are directories, record each of their dirEntries indexes
	dirIdxs := make([]int, 0, len(dirEntries))

	for idx, entry := range dirEntries {
		if entry.IsDir() {
			dirIdxs = append(dirIdxs, idx)
		}
	}

	if len(dirIdxs) == 0 {
		return ""
	}

	// Pick a random index from the list of indexes of DirEntry entries known to be directories.
	randDirIdx := dirIdxs[rand.Intn(len(dirIdxs))] //nolint:gosec
	randDirInfo := dirEntries[randDirIdx]

	return filepath.Join(dirPath, randDirInfo.Name())
}
