package files

import (
	"errors"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"

	"tea.chunkbyte.com/kato/go-worm/lib/config"
	"tea.chunkbyte.com/kato/go-worm/lib/helpers"
	"tea.chunkbyte.com/kato/go-worm/lib/models"
)

var (
	ErrInvalidPath = errors.New("path must be an absolute accessible Windows path")
	ErrOutsideRoot = errors.New("path is outside AGENT_FILE_ROOT")
)

func AllowedPath(root, raw string, requireExisting bool) (string, error) {
	if strings.TrimSpace(raw) == "" || !filepath.IsAbs(raw) {
		return "", ErrInvalidPath
	}
	clean := filepath.Clean(raw)
	if requireExisting {
		resolved, err := CanonicalExistingPath(clean)
		if err != nil {
			return "", err
		}
		clean = resolved
	}
	if root != "" && !isWithin(root, clean) {
		return "", ErrOutsideRoot
	}
	return clean, nil
}

func CanonicalExistingPath(path string) (string, error) {
	abs, err := filepath.Abs(filepath.Clean(path))
	if err != nil {
		return "", err
	}
	if _, err := os.Stat(abs); err != nil {
		return "", err
	}
	resolved, err := filepath.EvalSymlinks(abs)
	if err == nil {
		return filepath.Abs(resolved)
	}
	return abs, nil
}

func ParseDepth(raw string) (int, error) {
	if raw == "" {
		return 0, nil
	}
	depth, err := strconv.Atoi(raw)
	if err != nil || (depth != 0 && depth != 1) {
		return 0, errors.New("depth must be 0 or 1")
	}
	return depth, nil
}

func ListDirectory(dir string, depth int) ([]models.FileItem, error) {
	result := make([]models.FileItem, 0)
	var walk func(string, string, int) error
	walk = func(current, relative string, remaining int) error {
		entries, err := os.ReadDir(current)
		if err != nil {
			return err
		}
		for _, entry := range entries {
			if len(result) >= config.MaxListEntries {
				return errors.New("directory listing exceeds entry limit")
			}
			info, err := entry.Info()
			if err != nil {
				continue
			}
			entryPath := filepath.Join(current, entry.Name())
			rel := filepath.Join(relative, entry.Name())
			kind := "file"
			if info.IsDir() {
				kind = "dir"
			}
			result = append(result, models.FileItem{
				Name:     entry.Name(),
				Path:     rel,
				Type:     kind,
				Size:     info.Size(),
				Modified: info.ModTime().UTC(),
			})
			if remaining > 0 && info.IsDir() && entry.Type()&os.ModeSymlink == 0 {
				if err := walk(entryPath, rel, remaining-1); err != nil {
					return err
				}
			}
		}
		return nil
	}
	if err := walk(dir, "", depth); err != nil {
		return nil, err
	}
	sort.Slice(result, func(i, j int) bool {
		return strings.ToLower(result[i].Path) < strings.ToLower(result[j].Path)
	})
	return result, nil
}

func WritePathError(w http.ResponseWriter, err error) {
	switch {
	case errors.Is(err, ErrInvalidPath):
		helpers.WriteError(w, http.StatusBadRequest, err.Error())
	case errors.Is(err, ErrOutsideRoot), errors.Is(err, os.ErrPermission):
		helpers.WriteError(w, http.StatusForbidden, "path is not accessible")
	case errors.Is(err, os.ErrNotExist):
		helpers.WriteError(w, http.StatusNotFound, "path does not exist")
	default:
		helpers.WriteError(w, http.StatusBadRequest, "path is not accessible")
	}
}

func isWithin(root, candidate string) bool {
	rel, err := filepath.Rel(root, candidate)
	if err != nil {
		return false
	}
	return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) && !filepath.IsAbs(rel)
}
