package workspacefs

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

	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

const (
	maxWorkspaceFileSize = 1024 * 1024 // 1 MB
	DefaultListLimit     = 50
	MaxListLimit         = 200

	maxSeedFiles         = 20
	maxSeedFilePathRunes = 512
	maxSeedFileBytes     = maxWorkspaceFileSize
	maxSeedTotalBytes    = 4 * maxWorkspaceFileSize
)

// File represents a file or directory in an agent workspace.
type File struct {
	Name           string `json:"name"`
	Path           string `json:"path"`
	IsDirectory    bool   `json:"isDirectory"`
	Size           int64  `json:"size,omitempty"`
	ModifiedAt     string `json:"modifiedAt,omitempty"`
	ChildDirCount  int    `json:"childDirCount,omitempty"`
	ChildFileCount int    `json:"childFileCount,omitempty"`
}

// DirInfo summarizes one agent workspace directory.
type DirInfo struct {
	DirectoryName  string `json:"directoryName"`
	TotalSizeBytes int64  `json:"totalSizeBytes"`
	LastModified   string `json:"lastModified"`
	FileCount      int64  `json:"fileCount"`
}

// ListOptions bounds local workspace browse responses. Cursor is an offset
// token returned by a previous page.
type ListOptions struct {
	Limit  int
	Cursor string
}

// PageInfo describes whether a workspace browse response has another page.
type PageInfo struct {
	RootPath   string `json:"rootPath,omitempty"`
	Limit      int    `json:"limit"`
	Cursor     string `json:"cursor,omitempty"`
	HasMore    bool   `json:"hasMore"`
	NextCursor string `json:"nextCursor,omitempty"`
}

// textExtensions is a set of file extensions considered safe to read as text.
var textExtensions = map[string]struct{}{
	".txt": {}, ".md": {}, ".json": {}, ".yaml": {}, ".yml": {},
	".toml": {}, ".xml": {}, ".html": {}, ".css": {}, ".js": {},
	".ts": {}, ".tsx": {}, ".jsx": {}, ".go": {}, ".py": {},
	".rs": {}, ".java": {}, ".c": {}, ".h": {}, ".cpp": {},
	".sh": {}, ".bash": {}, ".zsh": {}, ".fish": {}, ".sql": {},
	".csv": {}, ".log": {}, ".env": {}, ".gitignore": {},
	".dockerignore": {}, ".editorconfig": {}, ".prettierrc": {},
	".eslintrc": {}, ".Makefile": {}, ".conf": {}, ".cfg": {},
	".ini": {}, ".properties": {}, ".gradle": {}, ".cmake": {},
	".proto": {}, ".graphql": {}, ".vue": {}, ".svelte": {},
	".scss": {}, ".less": {}, ".rb": {}, ".php": {}, ".swift": {},
	".kt": {}, ".dart": {}, ".lua": {}, ".r": {}, ".jl": {},
}

var hiddenWorkspaceEntries = map[string]struct{}{
	"AGENTS.md": {},
}

func shouldHideWorkspaceEntry(dirPath, name string) bool {
	if strings.TrimSpace(dirPath) != "" {
		return false
	}
	_, hidden := hiddenWorkspaceEntries[name]
	return hidden
}

// isTextExtension reports whether the given file extension is recognized as text.
func isTextExtension(ext string) bool {
	_, ok := textExtensions[ext]
	return ok
}

// WorkDir returns the workspace directory for the given agent.
func WorkDir(agentID string) string {
	return filepath.Join(profile.ConfigDir(), "agents", agentID)
}

// agentsBaseDir returns the base directory containing all agent workspaces.
func agentsBaseDir() string {
	return filepath.Join(profile.ConfigDir(), "agents")
}

// Deprecated: use ListTargetDirPage with an explicit limit/cursor.
func ListDir(agentID, dirPath string) ([]File, error) {
	return ListTargetDir(agentID, "", dirPath)
}

// ListTargetDir lists files and directories in the agent workspace or an explicit target.
//
// Deprecated: use ListTargetDirPage with an explicit limit/cursor.
func ListTargetDir(agentID, targetPath, dirPath string) ([]File, error) {
	return listTargetDirSorted(agentID, targetPath, dirPath)
}

// ListTargetDirPage lists files and directories in the agent workspace or an explicit target with pagination.
func ListTargetDirPage(agentID, targetPath, dirPath string, opts ListOptions) ([]File, PageInfo, error) {
	baseDir := workspaceBaseDir(agentID, targetPath)
	targetDir, err := safeJoin(baseDir, dirPath)
	if err != nil {
		return nil, PageInfo{}, err
	}
	files, page, err := listTargetDirPage(targetDir, strings.TrimSpace(targetPath) == "", dirPath, opts)
	if page.RootPath == "" {
		page.RootPath = baseDir
	}
	return files, page, err
}

func listTargetDirSorted(agentID, targetPath, dirPath string) ([]File, error) {
	baseDir := workspaceBaseDir(agentID, targetPath)
	targetDir, err := safeJoin(baseDir, dirPath)
	if err != nil {
		return nil, err
	}
	hideRootRuntimeFiles := strings.TrimSpace(targetPath) == ""
	entries, err := os.ReadDir(targetDir)
	if err != nil {
		if os.IsNotExist(err) {
			return []File{}, nil
		}
		return nil, err
	}

	var dirs, files []File
	for _, e := range entries {
		if !shouldIncludeDirEntry(e.Name(), hideRootRuntimeFiles, dirPath) {
			continue
		}
		node, ok := fileFromDirEntryInRoot(targetDir, e, dirPath)
		if !ok {
			continue
		}
		if node.IsDirectory {
			dirs = append(dirs, node)
		} else {
			files = append(files, node)
		}
	}

	// Sort: directories first, then files, each alphabetically.
	sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name < dirs[j].Name })
	sort.Slice(files, func(i, j int) bool { return files[i].Name < files[j].Name })

	result := make([]File, 0, len(dirs)+len(files))
	result = append(result, dirs...)
	result = append(result, files...)
	return result, nil
}

func listTargetDirPage(targetDir string, hideRootRuntimeFiles bool, dirPath string, opts ListOptions) ([]File, PageInfo, error) {
	opts = normalizeListOptions(opts)
	start := pageStart(opts.Cursor)
	dir, err := os.Open(targetDir)
	if err != nil {
		if os.IsNotExist(err) {
			return []File{}, pageInfoFor(0, opts, false), nil
		}
		return nil, PageInfo{}, err
	}
	defer dir.Close()

	files := make([]File, 0, opts.Limit)
	seen := 0
	for {
		entries, err := dir.ReadDir(opts.Limit + 1)
		if err != nil && err != io.EOF {
			return nil, PageInfo{}, err
		}
		if len(entries) == 0 {
			return files, pageInfoFor(start+len(files), opts, false), nil
		}
		for _, e := range entries {
			if !shouldIncludeDirEntry(e.Name(), hideRootRuntimeFiles, dirPath) {
				continue
			}
			if seen < start {
				seen++
				continue
			}
			node, ok := fileFromDirEntryInRoot(targetDir, e, dirPath)
			if !ok {
				continue
			}
			if len(files) >= opts.Limit {
				return files, pageInfoFor(start+len(files), opts, true), nil
			}
			files = append(files, node)
			seen++
		}
	}
}

func shouldIncludeDirEntry(name string, hideRootRuntimeFiles bool, dirPath string) bool {
	// Skip hidden files and node_modules.
	if strings.HasPrefix(name, ".") || name == "node_modules" {
		return false
	}
	return !hideRootRuntimeFiles || !shouldHideWorkspaceEntry(dirPath, name)
}

func fileFromDirEntry(e os.DirEntry, dirPath string) (File, bool) {
	return fileFromDirEntryInRoot("", e, dirPath)
}

func fileFromDirEntryInRoot(root string, e os.DirEntry, dirPath string) (File, bool) {
	info, err := e.Info()
	if err != nil {
		return File{}, false
	}
	name := e.Name()
	relPath := name
	if dirPath != "" {
		relPath = filepath.Join(dirPath, name)
	}
	node := File{
		Name:        name,
		Path:        relPath,
		IsDirectory: e.IsDir(),
		ModifiedAt:  info.ModTime().UTC().Format(time.RFC3339),
	}
	if !e.IsDir() {
		node.Size = info.Size()
	} else if strings.TrimSpace(root) != "" {
		node.ChildDirCount, node.ChildFileCount = countVisibleDirChildren(filepath.Join(root, name), relPath)
	}
	return node, true
}

func countVisibleDirChildren(dirPath string, relativeDirPath string) (int, int) {
	entries, err := os.ReadDir(dirPath)
	if err != nil {
		return 0, 0
	}
	dirCount := 0
	fileCount := 0
	for _, entry := range entries {
		if !shouldIncludeDirEntry(entry.Name(), false, relativeDirPath) {
			continue
		}
		if entry.IsDir() {
			dirCount++
		} else {
			fileCount++
		}
	}
	return dirCount, fileCount
}

// readWorkspaceTargetFile reads a single file from the agent workspace or an explicit target.
func ReadTargetFile(agentID, targetPath, filePath string) (string, bool, error) {
	baseDir := workspaceBaseDir(agentID, targetPath)
	target, err := safeJoin(baseDir, filePath)
	if err != nil {
		return "", false, err
	}

	info, err := os.Stat(target)
	if err != nil {
		return "", false, err
	}
	if info.IsDir() {
		return "", false, fmt.Errorf("cannot read a directory")
	}

	ext := strings.ToLower(filepath.Ext(target))
	if ext != "" && !isTextExtension(ext) {
		return "", true, nil
	}

	if info.Size() > maxWorkspaceFileSize {
		return "", false, fmt.Errorf("file too large (%d bytes, max %d)", info.Size(), maxWorkspaceFileSize)
	}

	data, err := os.ReadFile(target)
	if err != nil {
		return "", false, err
	}
	return string(data), false, nil
}

func workspaceBaseDir(agentID, targetPath string) string {
	trimmedTargetPath := strings.TrimSpace(targetPath)
	if trimmedTargetPath != "" {
		return normalizeUserPath(trimmedTargetPath)
	}
	return WorkDir(agentID)
}

func normalizeUserPath(path string) string {
	trimmed := strings.TrimSpace(path)
	if trimmed == "~" || strings.HasPrefix(trimmed, "~/") {
		if home, err := os.UserHomeDir(); err == nil && strings.TrimSpace(home) != "" {
			if trimmed == "~" {
				return filepath.Clean(home)
			}
			return filepath.Clean(filepath.Join(home, strings.TrimPrefix(trimmed, "~/")))
		}
	}
	return filepath.Clean(trimmed)
}

// ScanAll scans all agent workspace directories and returns sorted summary info.
//
// Deprecated: use ScanAllPage with an explicit limit/cursor.
func ScanAll() ([]DirInfo, error) {
	baseDir := agentsBaseDir()
	entries, err := os.ReadDir(baseDir)
	if err != nil {
		if os.IsNotExist(err) {
			return []DirInfo{}, nil
		}
		return nil, err
	}

	dirs := make([]os.DirEntry, 0, len(entries))
	for _, e := range entries {
		if e.IsDir() {
			dirs = append(dirs, e)
		}
	}
	sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
	return dirInfosForEntries(baseDir, dirs, 0), nil
}

// ScanAllPage scans a bounded page of agent workspace directories and returns summary info.
func ScanAllPage(opts ListOptions) ([]DirInfo, PageInfo, error) {
	opts = normalizeListOptions(opts)
	baseDir := agentsBaseDir()
	dir, err := os.Open(baseDir)
	if err != nil {
		if os.IsNotExist(err) {
			return []DirInfo{}, pageInfoFor(0, opts, false), nil
		}
		return nil, PageInfo{}, err
	}
	defer dir.Close()

	start := pageStart(opts.Cursor)
	dirs := make([]os.DirEntry, 0, opts.Limit)
	seen := 0
	for {
		entries, err := dir.ReadDir(opts.Limit + 1)
		if err != nil && err != io.EOF {
			return nil, PageInfo{}, err
		}
		if len(entries) == 0 {
			return dirInfosForEntries(baseDir, dirs, MaxListLimit), pageInfoFor(start+len(dirs), opts, false), nil
		}
		for _, e := range entries {
			if !e.IsDir() {
				continue
			}
			if seen < start {
				seen++
				continue
			}
			if len(dirs) >= opts.Limit {
				return dirInfosForEntries(baseDir, dirs, MaxListLimit), pageInfoFor(start+len(dirs), opts, true), nil
			}
			dirs = append(dirs, e)
			seen++
		}
	}
}

func dirInfosForEntries(baseDir string, entries []os.DirEntry, scanLimit int) []DirInfo {
	result := make([]DirInfo, 0, len(entries))
	for _, e := range entries {
		dirPath := filepath.Join(baseDir, e.Name())
		var lastMod time.Time
		var totalSize, fileCount int64
		if scanLimit > 0 {
			lastMod, totalSize, fileCount = ScanDirOneLevelBounded(dirPath, scanLimit)
		} else {
			lastMod, totalSize, fileCount = ScanDirOneLevel(dirPath)
		}
		result = append(result, DirInfo{
			DirectoryName:  e.Name(),
			TotalSizeBytes: totalSize,
			LastModified:   lastMod.UTC().Format(time.RFC3339),
			FileCount:      fileCount,
		})
	}
	return result
}

func normalizeListOptions(opts ListOptions) ListOptions {
	if opts.Limit <= 0 {
		opts.Limit = DefaultListLimit
	}
	if opts.Limit > MaxListLimit {
		opts.Limit = MaxListLimit
	}
	opts.Cursor = strings.TrimSpace(opts.Cursor)
	return opts
}

func pageStart(cursor string) int {
	var n int
	if _, err := fmt.Sscanf(strings.TrimSpace(cursor), "%d", &n); err != nil || n < 0 {
		return 0
	}
	return n
}

func pageInfoFor(nextOffset int, opts ListOptions, hasMore bool) PageInfo {
	opts = normalizeListOptions(opts)
	page := PageInfo{Limit: opts.Limit, Cursor: opts.Cursor, HasMore: hasMore}
	if hasMore {
		page.NextCursor = fmt.Sprintf("%d", nextOffset)
	}
	return page
}

// deleteWorkspaceDir deletes an agent workspace directory by name.
func DeleteDir(dirName string) (bool, error) {
	// Safety: directoryName must not contain path separators or traversal markers.
	if strings.Contains(dirName, "/") || strings.Contains(dirName, "\\") || strings.Contains(dirName, "..") {
		return false, fmt.Errorf("invalid directory name: %s", dirName)
	}

	target := filepath.Join(agentsBaseDir(), dirName)
	base := agentsBaseDir()
	if !strings.HasPrefix(target, base+string(os.PathSeparator)) {
		return false, fmt.Errorf("path traversal detected")
	}

	if err := os.RemoveAll(target); err != nil {
		return false, err
	}
	return true, nil
}

// applyWorkspaceSeedFiles writes or overwrites seed files inside an agent
// workspace, creating parent directories as needed.
func ApplySeedFiles(agentID string, seedFiles []types.WorkspaceSeedFile) error {
	baseDir := WorkDir(agentID)
	if err := validateSeedFiles(baseDir, seedFiles); err != nil {
		return err
	}

	if err := os.MkdirAll(baseDir, 0755); err != nil {
		return err
	}

	for _, seedFile := range seedFiles {
		trimmedPath := strings.TrimSpace(seedFile.Path)
		if trimmedPath == "" {
			return fmt.Errorf("seed file path is required")
		}

		target, err := safeJoin(baseDir, trimmedPath)
		if err != nil {
			return err
		}
		if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
			return err
		}
		if err := os.WriteFile(target, []byte(seedFile.Content), 0644); err != nil {
			return err
		}
	}

	return nil
}

func validateSeedFiles(baseDir string, seedFiles []types.WorkspaceSeedFile) error {
	if len(seedFiles) > maxSeedFiles {
		return fmt.Errorf("seed files has %d items, max %d", len(seedFiles), maxSeedFiles)
	}

	totalBytes := 0
	for i, seedFile := range seedFiles {
		trimmedPath := strings.TrimSpace(seedFile.Path)
		if trimmedPath == "" {
			return fmt.Errorf("seed file path is required")
		}
		if len([]rune(trimmedPath)) > maxSeedFilePathRunes {
			return fmt.Errorf("seed file path[%d] exceeds %d runes", i, maxSeedFilePathRunes)
		}
		if _, err := safeJoin(baseDir, trimmedPath); err != nil {
			return err
		}
		contentBytes := len([]byte(seedFile.Content))
		if contentBytes > maxSeedFileBytes {
			return fmt.Errorf("seed file %q exceeds %d bytes", trimmedPath, maxSeedFileBytes)
		}
		totalBytes += contentBytes
		if totalBytes > maxSeedTotalBytes {
			return fmt.Errorf("seed files exceed %d total bytes", maxSeedTotalBytes)
		}
	}
	return nil
}

// deleteWorkspacePath removes a file or directory under the agent workspace.
func DeletePath(agentID, workspacePath string) error {
	trimmedPath := strings.TrimSpace(workspacePath)
	if trimmedPath == "" {
		return fmt.Errorf("workspace path is required")
	}

	baseDir := WorkDir(agentID)
	target, err := safeJoin(baseDir, trimmedPath)
	if err != nil {
		return err
	}
	return os.RemoveAll(target)
}

// ResolvePathWithinBase resolves an arbitrary user-supplied path against base
// and rejects traversal or symlink escapes outside that base directory.
func ResolvePathWithinBase(base, path string) (string, error) {
	return safeJoin(base, path)
}

// safeJoin joins base and sub paths and validates the result stays within base.
func safeJoin(base, sub string) (string, error) {
	base = filepath.Clean(base)
	if sub == "" {
		return base, nil
	}
	resolved := ""
	if filepath.IsAbs(sub) {
		resolved = filepath.Clean(sub)
	} else {
		resolved = filepath.Clean(filepath.Join(base, sub))
	}
	if !pathWithinBase(base, resolved) {
		return "", fmt.Errorf("path traversal detected: %s", sub)
	}
	if err := ensureNoSymlinkEscape(base, resolved); err != nil {
		return "", err
	}
	return resolved, nil
}

func pathWithinBase(base, target string) bool {
	if target == base {
		return true
	}
	rel, err := filepath.Rel(base, target)
	if err != nil {
		return false
	}
	return rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator))
}

func ensureNoSymlinkEscape(base, target string) error {
	resolvedBase, err := filepath.EvalSymlinks(base)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}

	checkPath, err := existingPathForSymlinkCheck(target)
	if err != nil {
		return err
	}
	resolvedCheck, err := filepath.EvalSymlinks(checkPath)
	if err != nil {
		return err
	}
	if !pathWithinBase(resolvedBase, resolvedCheck) {
		return fmt.Errorf("path traversal detected through symlink: %s", target)
	}
	return nil
}

func existingPathForSymlinkCheck(path string) (string, error) {
	current := filepath.Clean(path)
	for {
		if _, err := os.Lstat(current); err == nil {
			return current, nil
		} else if !os.IsNotExist(err) {
			return "", err
		}
		parent := filepath.Dir(current)
		if parent == current {
			return "", fmt.Errorf("no existing parent for path: %s", path)
		}
		current = parent
	}
}

// ScanDirOneLevel scans a single directory level (non-recursive) for size stats.
// All entries (including hidden files) are counted. Directories contribute to
// lastMod but are excluded from totalSize and fileCount, matching the
// daemon-go / TS daemon behavior.
func ScanDirOneLevel(dir string) (lastMod time.Time, totalSize int64, fileCount int64) {
	return scanDirOneLevel(dir, 0)
}

// ScanDirOneLevelBounded scans at most limit directory entries for size stats.
func ScanDirOneLevelBounded(dir string, limit int) (lastMod time.Time, totalSize int64, fileCount int64) {
	return scanDirOneLevel(dir, limit)
}

func scanDirOneLevel(dir string, limit int) (lastMod time.Time, totalSize int64, fileCount int64) {
	if limit < 0 {
		limit = 0
	}
	if limit == 0 {
		return scanDirOneLevelSorted(dir)
	}

	f, err := os.Open(dir)
	if err != nil {
		return time.Time{}, 0, 0
	}
	defer f.Close()

	seen := 0
	for {
		entries, err := f.ReadDir(limit - seen)
		if err != nil && err != io.EOF {
			return time.Time{}, 0, 0
		}
		for _, e := range entries {
			seen++
			info, err := e.Info()
			if err != nil {
				if seen >= limit {
					return
				}
				continue
			}
			if info.ModTime().After(lastMod) {
				lastMod = info.ModTime()
			}
			if !e.IsDir() {
				totalSize += info.Size()
				fileCount++
			}
			if seen >= limit {
				return
			}
		}
		if err == io.EOF || len(entries) == 0 {
			return
		}
	}
}

func scanDirOneLevelSorted(dir string) (lastMod time.Time, totalSize int64, fileCount int64) {
	entries, err := os.ReadDir(dir)
	if err != nil {
		return time.Time{}, 0, 0
	}
	for _, e := range entries {
		info, err := e.Info()
		if err != nil {
			continue
		}
		if info.ModTime().After(lastMod) {
			lastMod = info.ModTime()
		}
		if !e.IsDir() {
			totalSize += info.Size()
			fileCount++
		}
	}
	return
}
