package file

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/enmanuel/agents/internal/config"
	"github.com/enmanuel/agents/tools"
)

// maxListEntries is the maximum number of entries returned by list_directory.
const maxListEntries = 500

// NewListDirectory creates a list_directory tool that lists files and directories.
// Deny-by-default: if AllowedPaths is empty, all listings are rejected.
// Does not follow symlinks that point outside of AllowedPaths.
func NewListDirectory(cfg config.FileOpsCfg) tools.Tool {
	return tools.Tool{
		Def: tools.Def{
			Name:        "list_directory",
			Description: "List files and directories at the given path. Returns name, size, type (file/dir), and modification date for each entry.",
			Parameters: []tools.Param{
				{Name: "path", Type: "string", Description: "Absolute path to the directory to list", Required: true},
				{Name: "recursive", Type: "boolean", Description: "List recursively (default: false)", Required: false},
			},
		},
		Exec: func(ctx context.Context, args map[string]any) tools.Result {
			path := tools.GetString(args, "path")
			if path == "" {
				return tools.Result{Err: fmt.Errorf("list_directory: path is required")}
			}

			recursive := getBool(args, "recursive")

			absPath, err := filepath.Abs(path)
			if err != nil {
				return tools.Result{Err: fmt.Errorf("list_directory: %w", err)}
			}

			if err := validatePath(absPath, cfg.AllowedPaths); err != nil {
				return tools.Result{Err: err}
			}

			info, err := os.Stat(absPath)
			if err != nil {
				return tools.Result{Err: fmt.Errorf("list_directory: %w", err)}
			}
			if !info.IsDir() {
				return tools.Result{Err: fmt.Errorf("list_directory: %q is not a directory", absPath)}
			}

			var entries []string
			if recursive {
				entries, err = listRecursive(absPath, cfg.AllowedPaths)
			} else {
				entries, err = listFlat(absPath, cfg.AllowedPaths)
			}
			if err != nil {
				return tools.Result{Err: fmt.Errorf("list_directory: %w", err)}
			}

			if len(entries) > maxListEntries {
				entries = entries[:maxListEntries]
				entries = append(entries, fmt.Sprintf("... (truncated, showing %d of more entries)", maxListEntries))
			}

			return tools.Result{Output: strings.Join(entries, "\n")}
		},
	}
}

// listFlat lists immediate children of dir.
func listFlat(dir string, allowedPaths []string) ([]string, error) {
	dirEntries, err := os.ReadDir(dir)
	if err != nil {
		return nil, err
	}

	var results []string
	for _, e := range dirEntries {
		entryPath := filepath.Join(dir, e.Name())

		// Skip symlinks that point outside allowed paths.
		if e.Type()&os.ModeSymlink != 0 {
			if err := validatePath(entryPath, allowedPaths); err != nil {
				continue
			}
		}

		info, err := e.Info()
		if err != nil {
			continue
		}

		results = append(results, formatEntry("", e.Name(), info))
	}
	return results, nil
}

// listRecursive lists all files under dir recursively.
func listRecursive(root string, allowedPaths []string) ([]string, error) {
	var results []string
	count := 0

	err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return nil // skip entries with errors
		}
		if path == root {
			return nil // skip the root directory itself
		}
		if count >= maxListEntries {
			return filepath.SkipAll
		}

		// Skip symlinks that point outside allowed paths.
		if d.Type()&os.ModeSymlink != 0 {
			if err := validatePath(path, allowedPaths); err != nil {
				if d.IsDir() {
					return filepath.SkipDir
				}
				return nil
			}
		}

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

		info, err := d.Info()
		if err != nil {
			return nil
		}

		results = append(results, formatEntry("", rel, info))
		count++
		return nil
	})

	return results, err
}

// formatEntry formats a single directory entry for output.
func formatEntry(prefix, name string, info os.FileInfo) string {
	kind := "file"
	if info.IsDir() {
		kind = "dir"
	}
	mod := info.ModTime().Format(time.RFC3339)
	display := name
	if prefix != "" {
		display = prefix + "/" + name
	}
	return fmt.Sprintf("%s\t%s\t%d\t%s", display, kind, info.Size(), mod)
}

// getBool extracts a boolean argument by name, returning false if missing or wrong type.
func getBool(args map[string]any, key string) bool {
	v, ok := args[key]
	if !ok {
		return false
	}
	b, ok := v.(bool)
	if !ok {
		return false
	}
	return b
}
