package fs

import (
	"context"
	"os"
	"path/filepath"
	"slices"
	"strings"

	sr "tildegit.org/tjp/sliderule"
	"tildegit.org/tjp/sliderule/gopher"
	"tildegit.org/tjp/sliderule/gopher/gophermap"
)

// GopherFileHandler builds a handler which serves up files from a file system.
//
// It only serves responses for paths which correspond to files, not directories.
func GopherFileHandler(fsroot, urlroot string, settings *gophermap.FileSystemSettings) sr.Handler {
	fsroot = strings.TrimRight(fsroot, "/")

	return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
		if !strings.HasPrefix(request.Path, urlroot) {
			return nil
		}
		requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")

		path := filepath.Join(fsroot, requestpath)
		if isPrivate(path) {
			return nil
		}
		if isf, err := isFile(path); err != nil {
			return gopher.Error(err).Response()
		} else if !isf {
			return nil
		}

		if settings == nil {
			settings = &gophermap.FileSystemSettings{}
		}

		file, err := os.Open(path)
		if err != nil {
			return gopher.Error(err).Response()
		}

		if !(settings.ParseExtended && isMap(path, *settings)) {
			return gopher.File(gopher.GuessItemType(path), file)
		}

		defer func() { _ = file.Close() }()

		edoc, err := gophermap.ParseExtended(file, request.URL)
		if err != nil {
			return gopher.Error(err).Response()
		}

		doc, _, err := edoc.Compatible(filepath.Dir(path), *settings)
		if err != nil {
			return gopher.Error(err).Response()
		}
		return doc.Response()
	})
}

// GopherDirectoryDefault serves up default files for directory path requests.
//
// If any of the supported filenames are found in the requested directory, the
// contents of that file is returned as the gopher response.
//
// It returns nil for any paths which don't correspond to a directory.
func GopherDirectoryDefault(fsroot, urlroot string, settings *gophermap.FileSystemSettings) sr.Handler {
	fsroot = strings.TrimRight(fsroot, "/")

	return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
		if !strings.HasPrefix(request.Path, urlroot) {
			return nil
		}
		requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")

		path := filepath.Join(fsroot, requestpath)
		if isPrivate(path) {
			return nil
		}
		if isd, err := isDir(path); err != nil {
			return gopher.Error(err).Response()
		} else if !isd {
			return nil
		}

		if settings == nil {
			settings = &gophermap.FileSystemSettings{}
		}

		for _, fname := range settings.DirMaps {
			fpath := filepath.Join(path, fname)
			if isf, err := isFile(fpath); err != nil {
				return gopher.Error(err).Response()
			} else if !isf {
				continue
			}

			file, err := os.Open(fpath)
			if err != nil {
				return gopher.Error(err).Response()
			}

			if settings.ParseExtended {
				defer func() { _ = file.Close() }()

				edoc, err := gophermap.ParseExtended(file, request.URL)
				if err != nil {
					return gopher.Error(err).Response()
				}

				doc, _, err := edoc.Compatible(path, *settings)
				if err != nil {
					return gopher.Error(err).Response()
				}
				return doc.Response()
			} else {
				return gopher.File(gopher.MenuType, file)
			}
		}

		return nil
	})
}

// GopherDirectoryListing produces a listing of the contents of any requested directories.
//
// It returns nil for any paths which don't correspond to a filesystem directory.
func GopherDirectoryListing(fsroot, urlroot string, settings *gophermap.FileSystemSettings) sr.Handler {
	fsroot = strings.TrimRight(fsroot, "/")

	return sr.HandlerFunc(func(ctx context.Context, request *sr.Request) *sr.Response {
		if !strings.HasPrefix(request.Path, urlroot) {
			return nil
		}
		requestpath := strings.Trim(strings.TrimPrefix(request.Path, urlroot), "/")

		path := filepath.Join(fsroot, requestpath)
		if isPrivate(path) {
			return nil
		}
		if isd, err := isDir(path); err != nil {
			return gopher.Error(err).Response()
		} else if !isd {
			return nil
		}

		if settings == nil {
			settings = &gophermap.FileSystemSettings{}
		}
		doc, err := gophermap.ListDir(path, request.URL, *settings)
		if err != nil {
			return gopher.Error(err).Response()
		}

		return doc.Response()
	})
}

func isDir(path string) (bool, error) {
	info, err := os.Stat(path)
	if err != nil {
		if isNotFound(err) {
			err = nil
		}
		return false, err
	}
	return info.IsDir() && info.Mode()&4 == 4, nil
}

func isFile(path string) (bool, error) {
	info, err := os.Stat(path)
	if err != nil {
		if isNotFound(err) {
			err = nil
		}
		return false, err
	}
	m := info.Mode()

	return m.IsRegular() && m&4 == 4, nil
}

func isMap(path string, settings gophermap.FileSystemSettings) bool {
	base := filepath.Base(path)
	if base == "gophermap" || strings.HasSuffix(base, ".gph") || strings.HasSuffix(base, ".gophermap") {
		return true
	}
	return slices.Contains(settings.DirMaps, filepath.Base(path))
}