// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.

package fpath

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"runtime"
	"slices"
	"strings"

	"github.com/zeebo/errs"
)

// IsRoot returns whether path is the root directory.
func IsRoot(path string) bool {
	abs, err := filepath.Abs(path)
	if err == nil {
		path = abs
	}

	return filepath.Dir(path) == path
}

// ApplicationDir returns best base directory for specific OS.
func ApplicationDir(subdir ...string) string {
	for i := range subdir {
		if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
			//lint:ignore SA1019 we cannot change the behavior and it doesn't make a big difference.
			//nolint:staticcheck // see above
			subdir[i] = strings.Title(subdir[i])
		} else {
			subdir[i] = strings.ToLower(subdir[i])
		}
	}
	var appdir string
	home := os.Getenv("HOME")

	switch runtime.GOOS {
	case "windows":
		// Windows standards: https://msdn.microsoft.com/en-us/library/windows/apps/hh465094.aspx?f=255&MSPPError=-2147217396
		for _, env := range []string{"AppData", "AppDataLocal", "UserProfile", "Home"} {
			val := os.Getenv(env)
			if val != "" {
				appdir = val
				break
			}
		}
	case "darwin":
		// Mac standards: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html
		appdir = filepath.Join(home, "Library", "Application Support")
	case "linux":
		fallthrough
	default:
		// Linux standards: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
		appdir = os.Getenv("XDG_DATA_HOME")
		if appdir == "" && home != "" {
			appdir = filepath.Join(home, ".local", "share")
		}
	}
	return filepath.Join(append([]string{appdir}, subdir...)...)
}

// IsValidSetupDir checks if directory is valid for setup configuration.
func IsValidSetupDir(name string) (ok bool, err error) {
	_, err = os.Stat(name)
	if err != nil {
		if os.IsNotExist(err) {
			return true, err
		}
		return false, err
	}

	/* #nosec G304 */ // The function limits later on paths having a config file
	f, err := os.Open(name)
	if err != nil {
		return false, err
	}
	defer func() {
		err = errs.Combine(err, f.Close())
	}()

	for {
		var filenames []string
		filenames, err = f.Readdirnames(100)
		if errors.Is(err, io.EOF) {
			// nothing more
			return true, nil
		} else if err != nil {
			// something went wrong
			return false, err
		}

		if slices.Contains(filenames, "config.yaml") {
			return false, nil
		}
	}
}

// IsWritable determines if a directory is writeable.
func IsWritable(filepath string) (bool, error) {
	info, err := os.Stat(filepath)
	if err != nil {
		return false, err
	}

	if !info.IsDir() {
		return false, fmt.Errorf("path %s is not a directory", filepath)
	}

	// Check if the user bit is enabled in file permission
	if info.Mode().Perm()&0200 == 0 {
		return false, errors.New("write permission bit is not set on this file for user")
	}

	// Test if user can create file
	// There is no OS cross-compatible method for
	// determining if a user has write permissions on a folder.
	// We can test by attempting to create a file in the folder.
	testFile := path.Join(filepath, ".perm")
	file, err := os.Create(testFile) // For read access.
	if err != nil {
		return false, errors.New("write permission bit is not set on this file for user")
	}

	_ = file.Close()
	_ = os.Remove(testFile)

	return true, nil
}
