package files

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

const DefaultMaxReadBytes = 1 << 20 // 1MB

var (
	ErrPathTraversal = errors.New("path escapes data root")
	ErrSymlink       = errors.New("symlinks are not allowed")
	ErrNotFound      = errors.New("file not found")
	ErrNotDirectory  = errors.New("path is not a directory")
	ErrIsDirectory   = errors.New("path is a directory")
)

// DirEntry describes a file or directory entry returned by ListDir.
type DirEntry struct {
	Name       string
	Path       string
	IsDir      bool
	Size       int64
	ModifiedAt time.Time
}

// ResolvePath resolves relativePath under dataRoot and rejects path traversal.
func ResolvePath(dataRoot, relativePath string) (string, error) {
	root, err := filepath.Abs(filepath.Clean(dataRoot))
	if err != nil {
		return "", fmt.Errorf("resolve data root: %w", err)
	}

	rel := strings.TrimPrefix(relativePath, "/")
	rel = strings.TrimPrefix(rel, `\`)
	rel = filepath.Clean(rel)
	if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
		return "", ErrPathTraversal
	}

	resolved := filepath.Join(root, rel)
	resolved, err = filepath.Abs(resolved)
	if err != nil {
		return "", fmt.Errorf("resolve path: %w", err)
	}

	if resolved != root && !strings.HasPrefix(resolved, root+string(filepath.Separator)) {
		return "", ErrPathTraversal
	}

	return resolved, nil
}

// ListDir lists entries in a directory relative to dataRoot.
func ListDir(dataRoot, relativePath string) ([]DirEntry, error) {
	dirPath, err := ResolvePath(dataRoot, relativePath)
	if err != nil {
		return nil, err
	}
	if err := rejectSymlinks(dataRoot, dirPath); err != nil {
		return nil, err
	}

	info, err := os.Lstat(dirPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, ErrNotFound
		}
		return nil, err
	}
	if !info.IsDir() {
		return nil, ErrNotDirectory
	}

	entries, err := os.ReadDir(dirPath)
	if err != nil {
		return nil, err
	}

	root, err := filepath.Abs(filepath.Clean(dataRoot))
	if err != nil {
		return nil, err
	}

	result := make([]DirEntry, 0, len(entries))
	for _, entry := range entries {
		entryPath := filepath.Join(dirPath, entry.Name())
		if err := rejectSymlinks(root, entryPath); err != nil {
			continue
		}

		entryInfo, err := entry.Info()
		if err != nil {
			return nil, err
		}

		rel, err := filepath.Rel(root, entryPath)
		if err != nil {
			return nil, err
		}
		rel = filepath.ToSlash(rel)

		result = append(result, DirEntry{
			Name:       entry.Name(),
			Path:       rel,
			IsDir:      entry.IsDir(),
			Size:       entryInfo.Size(),
			ModifiedAt: entryInfo.ModTime().UTC(),
		})
	}

	sort.Slice(result, func(i, j int) bool {
		if result[i].IsDir != result[j].IsDir {
			return result[i].IsDir
		}
		return strings.ToLower(result[i].Name) < strings.ToLower(result[j].Name)
	})

	return result, nil
}

// ReadFile reads file content relative to dataRoot up to maxBytes.
func ReadFile(dataRoot, relativePath string, maxBytes int) (content string, truncated bool, err error) {
	if maxBytes <= 0 {
		maxBytes = DefaultMaxReadBytes
	}

	filePath, err := ResolvePath(dataRoot, relativePath)
	if err != nil {
		return "", false, err
	}
	if err := rejectSymlinks(dataRoot, filePath); err != nil {
		return "", false, err
	}

	info, err := os.Lstat(filePath)
	if err != nil {
		if os.IsNotExist(err) {
			return "", false, ErrNotFound
		}
		return "", false, err
	}
	if info.IsDir() {
		return "", false, ErrIsDirectory
	}
	if info.Mode()&os.ModeSymlink != 0 {
		return "", false, ErrSymlink
	}

	f, err := os.Open(filePath)
	if err != nil {
		return "", false, err
	}
	defer f.Close()

	limited := io.LimitReader(f, int64(maxBytes)+1)
	data, err := io.ReadAll(limited)
	if err != nil {
		return "", false, err
	}
	if len(data) > maxBytes {
		truncated = true
		data = data[:maxBytes]
	}

	return string(data), truncated, nil
}

// WriteFile atomically writes content to a path relative to dataRoot.
func WriteFile(dataRoot, relativePath, content string, create bool) error {
	filePath, err := ResolvePath(dataRoot, relativePath)
	if err != nil {
		return err
	}

	if _, err := os.Lstat(filePath); err != nil {
		if os.IsNotExist(err) {
			if !create {
				return ErrNotFound
			}
		} else {
			return err
		}
	} else if !create {
		if err := rejectSymlinks(dataRoot, filePath); err != nil {
			return err
		}
	}

	dir := filepath.Dir(filePath)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}

	tmp, err := os.CreateTemp(dir, ".hexahost-write-*")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()

	cleanup := func() {
		_ = tmp.Close()
		_ = os.Remove(tmpPath)
	}

	if _, err := tmp.WriteString(content); err != nil {
		cleanup()
		return err
	}
	if err := tmp.Sync(); err != nil {
		cleanup()
		return err
	}
	if err := tmp.Close(); err != nil {
		cleanup()
		return err
	}
	if err := os.Rename(tmpPath, filePath); err != nil {
		cleanup()
		return err
	}

	return nil
}

func rejectSymlinks(dataRoot, target string) error {
	root, err := filepath.Abs(filepath.Clean(dataRoot))
	if err != nil {
		return err
	}
	target, err = filepath.Abs(target)
	if err != nil {
		return err
	}

	if target == root {
		info, err := os.Lstat(target)
		if err != nil {
			if os.IsNotExist(err) {
				return nil
			}
			return err
		}
		if info.Mode()&os.ModeSymlink != 0 {
			return fmt.Errorf("%w: %s", ErrSymlink, target)
		}
		return nil
	}

	rel, err := filepath.Rel(root, target)
	if err != nil {
		return err
	}

	current := root
	for _, part := range strings.Split(rel, string(filepath.Separator)) {
		if part == "" || part == "." {
			continue
		}
		current = filepath.Join(current, part)
		info, err := os.Lstat(current)
		if os.IsNotExist(err) {
			return nil
		}
		if err != nil {
			return err
		}
		if info.Mode()&os.ModeSymlink != 0 {
			return fmt.Errorf("%w: %s", ErrSymlink, current)
		}
	}

	return nil
}
