package fs

import (
	"bytes"
	"io"
	"io/fs"
	"sort"
	"strings"
	"text/template"

	sr "tildegit.org/tjp/sliderule"
)

// ResolveDirectory opens the directory corresponding to a request path.
//
// The string is the full path to the directory. If the returned ReadDirFile
// is not nil, it will be open and must be closed by the caller.
func ResolveDirectory(
	request *sr.Request,
	fileSystem fs.FS,
) (string, fs.ReadDirFile, error) {
	path := strings.Trim(request.Path, "/")
	if path == "" {
		path = "."
	}

	if isPrivate(path) {
		return "", nil, nil
	}

	file, err := fileSystem.Open(path)
	if isNotFound(err) {
		return "", nil, nil
	}
	if err != nil {
		return "", nil, err
	}

	isDir, err := fileIsDir(file)
	if err != nil {
		_ = file.Close()
		return "", nil, err
	}

	if !isDir {
		_ = file.Close()
		return "", nil, nil
	}

	dirFile, ok := file.(fs.ReadDirFile)
	if !ok {
		_ = file.Close()
		return "", nil, nil
	}

	return path, dirFile, nil
}

// ResolveDirectoryDefault finds any of the provided filenames within a directory.
//
// If it does not find any of the filenames it returns "", nil, nil.
func ResolveDirectoryDefault(
	fileSystem fs.FS,
	dirPath string,
	dir fs.ReadDirFile,
	filenames []string,
) (string, fs.File, error) {
	entries, err := dir.ReadDir(0)
	if err != nil {
		return "", nil, err
	}
	sort.Slice(entries, func(a, b int) bool {
		return entries[a].Name() < entries[b].Name()
	})

	for _, filename := range filenames {
		idx := sort.Search(len(entries), func(i int) bool {
			return entries[i].Name() >= filename
		})

		if idx < len(entries) && entries[idx].Name() == filename {
			path := strings.TrimLeft(dirPath+"/"+filename, "./")
			file, err := fileSystem.Open(path)
			return path, file, err
		}
	}

	return "", nil, nil
}

// RenderDirectoryListing provides an io.Reader with the output of a directory listing template.
//
// The template is provided the following namespace:
//   - .FullPath: the complete path to the listed directory
//   - .DirName: the name of the directory itself
//   - .Entries: the []fs.DirEntry of the directory contents
//   - .Hostname: the hostname of the server hosting the file
//   - .Port: the port on which the server is listening
//
// Each entry in .Entries has the following fields:
//   - .Name the string name of the item within the directory
//   - .IsDir is a boolean
//   - .Type is the FileMode bits
//   - .Info is a method returning (fs.FileInfo, error)
func RenderDirectoryListing(
	path string,
	dir fs.ReadDirFile,
	template *template.Template,
	server sr.Server,
) (io.Reader, error) {
	buf := &bytes.Buffer{}

	environ, err := dirlistNamespace(path, dir, server)
	if err != nil {
		return nil, err
	}

	if err := template.Execute(buf, environ); err != nil {
		return nil, err
	}

	return buf, nil
}

func dirlistNamespace(path string, dirFile fs.ReadDirFile, server sr.Server) (map[string]any, error) {
	entries, err := dirFile.ReadDir(0)
	if err != nil {
		return nil, err
	}

	for i := len(entries) - 1; i >= 0; i-- {
		if strings.HasPrefix(entries[i].Name(), ".") {
			copy(entries[i:], entries[i+1:])
			entries = entries[:len(entries)-1]
		}
	}

	sort.Slice(entries, func(i, j int) bool {
		return entries[i].Name() < entries[j].Name()
	})

	var dirname string
	if path == "." {
		dirname = "(root)"
	} else {
		dirname = path[strings.LastIndex(path, "/")+1:]
	}

	hostname := "none"
	port := "0"
	if server != nil {
		hostname = server.Hostname()
		port = server.Port()
	}

	m := map[string]any{
		"FullPath": path,
		"DirName":  dirname,
		"Entries":  entries,
		"Hostname": hostname,
		"Port":     port,
	}

	return m, nil
}