package dashboard

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/desktop/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
)

const (
	maxAgentWorkspacePreviewBytes    = 1024 * 1024
	agentWorkspaceDisplayRootPattern = "~/.agentteam/agents/%s"
)

// NewAgentWorkspaceTreeHandler creates a handler that lists one directory level
// under ~/.agentteam/agents/{id}. Pass dirPath to browse children lazily.
func NewAgentWorkspaceTreeHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.Param("id")
		if _, ok := profile.Default().Agents().Get(id); !ok {
			c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("agent %q not found", id)})
			return
		}

		root, err := agentWorkspaceRoot(id)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		dirPath := strings.TrimSpace(c.Query("dirPath"))
		nodes, err := listAgentWorkspaceNodes(root, dirPath)
		if err != nil {
			if strings.Contains(err.Error(), "path traversal") {
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("list workspace: %v", err)})
			return
		}

		c.JSON(http.StatusOK, dto.AgentWorkspaceTreeResponse{
			Root:  fmt.Sprintf(agentWorkspaceDisplayRootPattern, id),
			Nodes: nodes,
		})
	}
}

// NewAgentWorkspaceFileHandler creates a handler that previews one file under
// ~/.agentteam/agents/{id}. The file path is provided as the "path" query parameter.
func NewAgentWorkspaceFileHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		id := c.Param("id")
		if _, ok := profile.Default().Agents().Get(id); !ok {
			c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("agent %q not found", id)})
			return
		}

		root, err := agentWorkspaceRoot(id)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		relPath := strings.TrimSpace(c.Query("path"))
		target, err := safeAgentWorkspacePath(root, relPath)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		info, err := os.Stat(target)
		if os.IsNotExist(err) {
			c.JSON(http.StatusNotFound, gin.H{"error": "file not found"})
			return
		}
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if info.IsDir() {
			c.JSON(http.StatusBadRequest, gin.H{"error": "path is a directory"})
			return
		}

		file, err := os.Open(target)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("open file: %v", err)})
			return
		}
		defer file.Close()

		data, err := io.ReadAll(io.LimitReader(file, maxAgentWorkspacePreviewBytes+1))
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("read file: %v", err)})
			return
		}
		truncated := int64(len(data)) > maxAgentWorkspacePreviewBytes
		if truncated {
			data = data[:maxAgentWorkspacePreviewBytes]
		}

		normalizedPath := filepath.ToSlash(filepath.Clean(relPath))
		resp := dto.AgentWorkspaceFileResponse{
			Path:       normalizedPath,
			Name:       filepath.Base(target),
			Size:       info.Size(),
			ModifiedAt: info.ModTime().UTC().Format(time.RFC3339),
			Truncated:  truncated,
		}
		if !utf8.Valid(data) {
			resp.Binary = true
			c.JSON(http.StatusOK, resp)
			return
		}
		resp.Content = string(data)
		c.JSON(http.StatusOK, resp)
	}
}

func listAgentWorkspaceNodes(root, rel string) ([]dto.AgentWorkspaceNode, error) {
	dir, err := safeAgentWorkspacePath(root, rel)
	if err != nil {
		return nil, err
	}

	entries, err := os.ReadDir(dir)
	if os.IsNotExist(err) {
		return []dto.AgentWorkspaceNode{}, nil
	}
	if err != nil {
		return nil, err
	}

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

	nodes := make([]dto.AgentWorkspaceNode, 0, len(entries))
	for _, entry := range entries {
		info, err := entry.Info()
		if err != nil {
			continue
		}
		name := entry.Name()
		path := filepath.ToSlash(filepath.Join(rel, name))
		node := dto.AgentWorkspaceNode{
			Name:       name,
			Path:       path,
			IsDir:      entry.IsDir(),
			ModifiedAt: info.ModTime().UTC().Format(time.RFC3339),
		}
		if !entry.IsDir() {
			node.Size = info.Size()
		}
		nodes = append(nodes, node)
	}
	return nodes, nil
}

func agentWorkspaceRoot(agentID string) (string, error) {
	agentsRoot, err := filepath.Abs(filepath.Join(profile.ConfigDir(), "agents"))
	if err != nil {
		return "", err
	}
	root, err := filepath.Abs(profile.AgentWorkDir(agentID))
	if err != nil {
		return "", err
	}
	rel, err := filepath.Rel(agentsRoot, root)
	if err != nil {
		return "", err
	}
	if rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
		return "", fmt.Errorf("invalid agent workspace path")
	}
	return root, nil
}

func safeAgentWorkspacePath(root, rel string) (string, error) {
	cleanRel := filepath.Clean(strings.TrimSpace(rel))
	if cleanRel == "." {
		cleanRel = ""
	}
	if filepath.IsAbs(cleanRel) {
		return "", fmt.Errorf("invalid file path")
	}
	target := filepath.Join(root, cleanRel)
	absRoot, err := filepath.Abs(root)
	if err != nil {
		return "", err
	}
	absTarget, err := filepath.Abs(target)
	if err != nil {
		return "", err
	}
	relToRoot, err := filepath.Rel(absRoot, absTarget)
	if err != nil {
		return "", err
	}
	if strings.HasPrefix(relToRoot, "..") || filepath.IsAbs(relToRoot) {
		return "", fmt.Errorf("path traversal detected")
	}

	evaluatedRoot, err := filepath.EvalSymlinks(absRoot)
	if err != nil {
		if os.IsNotExist(err) {
			return absTarget, nil
		}
		return "", err
	}
	evaluatedTarget, err := filepath.EvalSymlinks(absTarget)
	if err != nil {
		if os.IsNotExist(err) {
			return absTarget, nil
		}
		return "", err
	}
	relEvaluated, err := filepath.Rel(evaluatedRoot, evaluatedTarget)
	if err != nil {
		return "", err
	}
	if strings.HasPrefix(relEvaluated, "..") || filepath.IsAbs(relEvaluated) {
		return "", fmt.Errorf("path traversal detected")
	}
	return absTarget, nil
}
