package dashboard

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strconv"
	"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 (
	maxSkillFileEntries      = 1000
	maxSkillFilePreviewBytes = 1024 * 1024
	skillDisplayRootPattern  = "~/.agentteam/skills/%s"
)

var (
	skillImportUploadMaxBytes = profile.MaxSkillImportBytes
	errSkillUploadTooLarge    = errors.New("uploaded skill file exceeds size limit")
)

// ---------------------------------------------------------------------------
// GET /skills
// ---------------------------------------------------------------------------

// ListSkillsHandler handles listing all skills.
type ListSkillsHandler struct{}

// NewListSkillsHandler returns a handler that lists all skills.
func NewListSkillsHandler() gin.HandlerFunc {
	return (&ListSkillsHandler{}).Handle
}

// Handle processes the list skills request.
func (h *ListSkillsHandler) Handle(c *gin.Context) {
	items := profile.Default().Skills().All()
	resp := make([]dto.SkillResponse, 0, len(items))
	for _, s := range items {
		resp = append(resp, buildSkillResponse(s))
	}
	c.JSON(http.StatusOK, resp)
}

// ---------------------------------------------------------------------------
// GET /skills/:id
// ---------------------------------------------------------------------------

// GetSkillHandler handles getting a single skill by ID.
type GetSkillHandler struct{}

// NewGetSkillHandler returns a handler that gets a skill by ID.
func NewGetSkillHandler() gin.HandlerFunc {
	return (&GetSkillHandler{}).Handle
}

// Handle processes the get skill request.
func (h *GetSkillHandler) Handle(c *gin.Context) {
	id := c.Param("id")
	s, ok := profile.Default().Skills().Get(id)
	if !ok {
		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("skill %q not found", id)})
		return
	}
	c.JSON(http.StatusOK, buildSkillResponse(s))
}

// ---------------------------------------------------------------------------
// GET /skills/:id/files/tree
// ---------------------------------------------------------------------------

// SkillFilesTreeHandler handles listing the imported files for a skill.
type SkillFilesTreeHandler struct{}

// NewSkillFilesTreeHandler returns a handler that lists the imported skill files.
func NewSkillFilesTreeHandler() gin.HandlerFunc {
	return (&SkillFilesTreeHandler{}).Handle
}

// Handle processes the skill file tree request.
func (h *SkillFilesTreeHandler) Handle(c *gin.Context) {
	id := c.Param("id")
	if _, ok := profile.Default().Skills().Get(id); !ok {
		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("skill %q not found", id)})
		return
	}

	root, err := skillFilesRoot(id)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	if _, err := os.Stat(root); os.IsNotExist(err) {
		c.JSON(http.StatusOK, dto.SkillFilesTreeResponse{
			Root:  fmt.Sprintf(skillDisplayRootPattern, id),
			Nodes: []dto.SkillFileNode{},
		})
		return
	}

	count := 0
	nodes, truncated, err := listSkillFileNodes(root, "", &count)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("list skill files: %v", err)})
		return
	}

	c.JSON(http.StatusOK, dto.SkillFilesTreeResponse{
		Root:      fmt.Sprintf(skillDisplayRootPattern, id),
		Nodes:     nodes,
		Truncated: truncated,
	})
}

// ---------------------------------------------------------------------------
// GET /skills/:id/files/file
// ---------------------------------------------------------------------------

// SkillFileHandler handles previewing one imported skill file.
type SkillFileHandler struct{}

// NewSkillFileHandler returns a handler that previews one imported skill file.
func NewSkillFileHandler() gin.HandlerFunc {
	return (&SkillFileHandler{}).Handle
}

// Handle processes the skill file preview request.
func (h *SkillFileHandler) Handle(c *gin.Context) {
	id := c.Param("id")
	if _, ok := profile.Default().Skills().Get(id); !ok {
		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("skill %q not found", id)})
		return
	}

	root, err := skillFilesRoot(id)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	relPath := strings.TrimSpace(c.Query("path"))
	target, err := safeSkillFilePath(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, maxSkillFilePreviewBytes+1))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("read file: %v", err)})
		return
	}
	truncated := len(data) > maxSkillFilePreviewBytes
	if truncated {
		data = data[:maxSkillFilePreviewBytes]
	}

	normalizedPath := filepath.ToSlash(filepath.Clean(relPath))
	resp := dto.SkillFileResponse{
		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)
}

// ---------------------------------------------------------------------------
// POST /skills/import
// ---------------------------------------------------------------------------

// ImportSkillHandler handles importing skills from an uploaded file or URL.
type ImportSkillHandler struct{}

// NewImportSkillHandler returns a handler that imports from a file upload or URL.
func NewImportSkillHandler() gin.HandlerFunc {
	return (&ImportSkillHandler{}).Handle
}

type skillImportSource struct {
	url   string
	files []skillImportFile
}

type skillImportFile struct {
	file     multipart.File
	filename string
}

func openSkillImportSource(c *gin.Context) (skillImportSource, bool) {
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, skillImportUploadMaxBytes)

	skillURL := strings.TrimSpace(c.PostForm("url"))
	if err := c.Request.ParseMultipartForm(32 << 20); err != nil && isRequestBodyTooLarge(err) {
		c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": errSkillUploadTooLarge.Error()})
		return skillImportSource{}, false
	}

	fileHeaders := multipartFormFiles(c, "file")
	if skillURL == "" && len(fileHeaders) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "file upload or url is required; use multipart/form-data with a 'file' field or provide a 'url' form field",
		})
		return skillImportSource{}, false
	}
	if skillURL != "" && len(fileHeaders) > 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "provide either a file upload or a url, not both"})
		return skillImportSource{}, false
	}
	if skillURL != "" {
		return skillImportSource{url: skillURL}, true
	}

	files := make([]skillImportFile, 0, len(fileHeaders))
	for _, header := range fileHeaders {
		file, err := header.Open()
		if err != nil {
			for _, upload := range files {
				_ = upload.file.Close()
			}
			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("open uploaded file %q: %v", header.Filename, err)})
			return skillImportSource{}, false
		}
		files = append(files, skillImportFile{file: file, filename: header.Filename})
	}
	return skillImportSource{files: files}, true
}

func (s skillImportSource) close() {
	for _, file := range s.files {
		_ = file.file.Close()
	}
}

func saveSkillUploadToTemp(r io.Reader, filename string, pattern string) (string, error) {
	tmpDir, err := os.MkdirTemp(os.TempDir(), pattern)
	if err != nil {
		return "", fmt.Errorf("create temporary file: %w", err)
	}
	tmpPath := filepath.Join(tmpDir, safeUploadFilename(filename))
	tmpFile, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
	if err != nil {
		_ = os.RemoveAll(tmpDir)
		return "", fmt.Errorf("create temporary file: %w", err)
	}
	cleanup := true
	defer func() {
		if cleanup {
			_ = os.RemoveAll(tmpDir)
		}
	}()

	written, copyErr := io.Copy(tmpFile, io.LimitReader(r, skillImportUploadMaxBytes+1))
	closeErr := tmpFile.Close()
	if copyErr != nil {
		if isRequestBodyTooLarge(copyErr) {
			return "", errSkillUploadTooLarge
		}
		return "", fmt.Errorf("save uploaded file: %w", copyErr)
	}
	if written > skillImportUploadMaxBytes {
		return "", errSkillUploadTooLarge
	}
	if closeErr != nil {
		return "", fmt.Errorf("finalize uploaded file: %w", closeErr)
	}
	cleanup = false
	return tmpPath, nil
}

func safeUploadFilename(filename string) string {
	normalized := strings.ReplaceAll(filepath.ToSlash(strings.TrimSpace(filename)), `\`, "/")
	name := path.Base(normalized)
	if name == "." || name == ".." || name == "/" || name == "" {
		return "skill.md"
	}
	return name
}

func removeSkillUploadTemp(tmpPath string) {
	if tmpPath == "" {
		return
	}
	_ = os.RemoveAll(filepath.Dir(tmpPath))
}

func writeSkillUploadError(c *gin.Context, err error) {
	if errors.Is(err, errSkillUploadTooLarge) {
		c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": errSkillUploadTooLarge.Error()})
		return
	}
	message := "failed to save uploaded file"
	if strings.Contains(err.Error(), "create temporary file") {
		message = "failed to create temporary file"
	}
	if strings.Contains(err.Error(), "finalize uploaded file") {
		message = "failed to finalize uploaded file"
	}
	c.JSON(http.StatusInternalServerError, gin.H{"error": message})
}

func isRequestBodyTooLarge(err error) bool {
	return err != nil && strings.Contains(strings.ToLower(err.Error()), "request body too large")
}

func multipartFormFiles(c *gin.Context, key string) []*multipart.FileHeader {
	if c.Request.MultipartForm == nil || c.Request.MultipartForm.File == nil {
		return nil
	}
	return c.Request.MultipartForm.File[key]
}

func skillImportMetaFromForm(c *gin.Context) *profile.SkillMeta {
	meta := &profile.SkillMeta{
		ID:          c.PostForm("id"),
		Name:        c.PostForm("name"),
		Description: c.PostForm("description"),
		Version:     c.PostForm("version"),
		Author:      c.PostForm("author"),
	}
	if tags := c.PostForm("tags"); tags != "" {
		meta.Tags = splitTags(tags)
	}
	return meta
}

func skillImportForceFromForm(c *gin.Context) bool {
	if v := c.PostForm("force"); v != "" {
		force, _ := strconv.ParseBool(v)
		return force
	}
	return false
}

func skillImportMetaHasFields(meta *profile.SkillMeta) bool {
	return meta != nil &&
		(strings.TrimSpace(meta.ID) != "" ||
			strings.TrimSpace(meta.Name) != "" ||
			strings.TrimSpace(meta.Description) != "" ||
			strings.TrimSpace(meta.Version) != "" ||
			strings.TrimSpace(meta.Author) != "" ||
			len(meta.Tags) > 0)
}

// Handle processes the skill import request.
// It accepts multipart/form-data with a "file" field OR a "url" form field
// to import from a remote URL. Optional metadata fields (id, name, description,
// version, author, tags) are used when the file lacks front-matter.
func (h *ImportSkillHandler) Handle(c *gin.Context) {
	source, ok := openSkillImportSource(c)
	if !ok {
		return
	}
	defer source.close()

	var tmpPaths []string
	var err error
	if source.url == "" {
		tmpPaths = make([]string, 0, len(source.files))
		for _, upload := range source.files {
			tmpPath, err := saveSkillUploadToTemp(upload.file, upload.filename, "agentteam-skill-*")
			if err != nil {
				for _, tmpPath := range tmpPaths {
					removeSkillUploadTemp(tmpPath)
				}
				writeSkillUploadError(c, err)
				return
			}
			tmpPaths = append(tmpPaths, tmpPath)
		}
		defer func() {
			for _, tmpPath := range tmpPaths {
				removeSkillUploadTemp(tmpPath)
			}
		}()
	}

	// Build metadata from form fields.
	meta := skillImportMetaFromForm(c)
	force := skillImportForceFromForm(c)

	if len(tmpPaths) > 1 {
		if skillImportMetaHasFields(meta) {
			c.JSON(http.StatusBadRequest, gin.H{"error": "metadata overrides are only supported when importing a single skill file"})
			return
		}
		resp := importSkillFilesBatch(tmpPaths, force)
		c.JSON(http.StatusCreated, resp)
		return
	}

	var result *profile.AutoImportResult
	if source.url != "" {
		result, err = profile.Default().Skills().ImportAutoFromURL(source.url, meta, force)
	} else {
		result, err = profile.Default().Skills().ImportAutoFromFile(tmpPaths[0], meta, force)
	}
	if err != nil {
		if errors.Is(err, profile.ErrSkillExists) {
			c.JSON(http.StatusConflict, gin.H{
				"error":    err.Error(),
				"exists":   true,
				"hint":     "A skill with this ID already exists. Set force=true to overwrite.",
				"required": []string{"id", "name"},
				"optional": []string{"description", "version", "author", "tags"},
			})
			return
		}
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	if result.NeedsMeta {
		c.JSON(http.StatusUnprocessableEntity, gin.H{
			"error":     "source does not contain required metadata",
			"needsMeta": true,
			"hint":      "The selected source lacks required metadata (id, name). Please provide them as form fields or add YAML front-matter to the skill file.",
			"required":  []string{"id", "name"},
			"optional":  []string{"description", "version", "author", "tags"},
		})
		return
	}

	if result.Batch != nil {
		resp := dto.BatchImportResponse{
			Imported: make([]dto.BatchImportResult, len(result.Batch.Imported)),
			Created:  result.Batch.Created,
			Skipped:  result.Batch.Skipped,
			Errors:   result.Batch.Errors,
		}
		for i, imported := range result.Batch.Imported {
			resp.Imported[i] = dto.BatchImportResult{
				ID:     imported.ID,
				Name:   imported.Name,
				Status: imported.Status,
			}
		}
		c.JSON(http.StatusCreated, resp)
		return
	}

	if result.Skill == nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "skill import returned no result"})
		return
	}

	c.JSON(http.StatusCreated, buildSkillResponse(*result.Skill))
}

func importSkillFilesBatch(filePaths []string, force bool) dto.BatchImportResponse {
	resp := dto.BatchImportResponse{}
	for _, filePath := range filePaths {
		result, err := profile.Default().Skills().ImportAutoFromFile(filePath, nil, force)
		if err != nil {
			if errors.Is(err, profile.ErrSkillExists) {
				resp.Imported = append(resp.Imported, dto.BatchImportResult{
					ID:     filepath.Base(filePath),
					Name:   filepath.Base(filePath),
					Status: "skipped_exists",
				})
				resp.Skipped++
				continue
			}
			resp.Errors = append(resp.Errors, fmt.Sprintf("%s: %v", filepath.Base(filePath), err))
			continue
		}
		if result.NeedsMeta {
			resp.Errors = append(resp.Errors, fmt.Sprintf("%s: source does not contain required metadata", filepath.Base(filePath)))
			continue
		}
		if result.Batch != nil {
			for _, imported := range result.Batch.Imported {
				resp.Imported = append(resp.Imported, dto.BatchImportResult{
					ID:     imported.ID,
					Name:   imported.Name,
					Status: imported.Status,
				})
			}
			resp.Created += result.Batch.Created
			resp.Skipped += result.Batch.Skipped
			resp.Errors = append(resp.Errors, result.Batch.Errors...)
			continue
		}
		if result.Skill == nil {
			resp.Errors = append(resp.Errors, fmt.Sprintf("%s: skill import returned no result", filepath.Base(filePath)))
			continue
		}
		resp.Imported = append(resp.Imported, dto.BatchImportResult{
			ID:     result.Skill.ID,
			Name:   result.Skill.Name,
			Status: "created",
		})
		resp.Created++
	}
	return resp
}

// ---------------------------------------------------------------------------
// POST /skills/import-stream (SSE)
// ---------------------------------------------------------------------------

// ImportSkillStreamHandler handles streaming skill import via SSE.
type ImportSkillStreamHandler struct{}

// NewImportSkillStreamHandler returns a handler that streams URL-based skill import.
func NewImportSkillStreamHandler() gin.HandlerFunc {
	return (&ImportSkillStreamHandler{}).Handle
}

func (h *ImportSkillStreamHandler) Handle(c *gin.Context) {
	skillURL := c.PostForm("url")
	if skillURL == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "url form field is required"})
		return
	}

	meta := skillImportMetaFromForm(c)
	force := skillImportForceFromForm(c)

	startDashboardEventStream(c)

	pr, pw := io.Pipe()
	go func() {
		defer pw.Close()
		result, err := profile.Default().Skills().ImportAutoFromURLTo(skillURL, meta, force, pw)
		enc := json.NewEncoder(pw)
		if err != nil {
			payload := map[string]string{"_type": "error", "message": err.Error()}
			if errors.Is(err, profile.ErrSkillExists) {
				payload["code"] = "conflict"
			}
			var unsupportedHost *profile.UnsupportedHostError
			if errors.As(err, &unsupportedHost) {
				payload["code"] = "unsupported_host"
			}
			_ = enc.Encode(payload)
			return
		}
		_ = enc.Encode(map[string]any{"_type": "done", "needsMeta": result.NeedsMeta})
	}()

	defer pr.Close()
	streamDashboardPipe(c, pr, writeSkillImportStreamLine)
}

func writeSkillImportStreamLine(c *gin.Context, line string) {
	if line == "" {
		return
	}
	var msg map[string]any
	if err := json.Unmarshal([]byte(line), &msg); err == nil {
		if msgType, ok := msg["_type"].(string); ok {
			switch msgType {
			case "done":
				needsMeta, _ := msg["needsMeta"].(bool)
				writeDashboardSSE(c, "done", fmt.Sprintf(`{"success":true,"needsMeta":%v}`, needsMeta))
				return
			case "error":
				errMsg, _ := msg["message"].(string)
				code, _ := msg["code"].(string)
				writeDashboardSSE(c, "error", fmt.Sprintf(`{"message":"%s","code":"%s"}`, escapeJSON(errMsg), escapeJSON(code)))
				return
			}
		}
	}
	writeDashboardLogEvent(c, line)
}

// ---------------------------------------------------------------------------
// PUT /skills/:id
// ---------------------------------------------------------------------------

// UpdateSkillHandler handles updating a skill's metadata.
type UpdateSkillHandler struct{}

// NewUpdateSkillHandler returns a handler that updates a skill.
func NewUpdateSkillHandler() gin.HandlerFunc {
	return (&UpdateSkillHandler{}).Handle
}

// Handle processes the update skill request.
func (h *UpdateSkillHandler) Handle(c *gin.Context) {
	id := c.Param("id")
	existing, ok := profile.Default().Skills().Get(id)
	if !ok {
		c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("skill %q not found", id)})
		return
	}

	var req dto.SkillResponse
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid request body: %v", err)})
		return
	}

	// Preserve immutable fields.
	s := existing
	if req.Name != "" {
		s.Name = req.Name
	}
	s.Description = req.Description
	s.Version = req.Version
	s.Author = req.Author
	s.Tags = req.Tags

	if err := profile.Default().Skills().Update(s); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, buildSkillResponse(s))
}

// ---------------------------------------------------------------------------
// DELETE /skills/:id
// ---------------------------------------------------------------------------

// DeleteSkillHandler handles deleting a skill.
type DeleteSkillHandler struct{}

// NewDeleteSkillHandler returns a handler that deletes a skill.
func NewDeleteSkillHandler() gin.HandlerFunc {
	return (&DeleteSkillHandler{}).Handle
}

// Handle processes the delete skill request.
func (h *DeleteSkillHandler) Handle(c *gin.Context) {
	id := c.Param("id")
	if err := profile.Default().Skills().Delete(id); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"status": "deleted", "id": id})
}

// ---------------------------------------------------------------------------
// POST /skills/import-batch
// ---------------------------------------------------------------------------

// ImportBatchSkillHandler handles batch importing skills from an archive.
type ImportBatchSkillHandler struct{}

// NewImportBatchSkillHandler returns a handler that batch-imports skills from an archive.
func NewImportBatchSkillHandler() gin.HandlerFunc {
	return (&ImportBatchSkillHandler{}).Handle
}

// Handle processes the batch import request.
// It accepts multipart/form-data with a "file" field OR a "url" form field
// (supporting GitHub/GitLab repo URLs). Optional "force" parameter controls
// whether to overwrite existing skills.
func (h *ImportBatchSkillHandler) Handle(c *gin.Context) {
	source, ok := openSkillImportSource(c)
	if !ok {
		return
	}
	defer source.close()

	var tmpPath string
	if source.url != "" {
		// Resolve GitHub/GitLab repo URL to archive URL.
		archiveURL, err := profile.ResolveArchiveURL(source.url)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid URL: %v", err)})
			return
		}
		tmpPath, err = profile.DownloadToFile(archiveURL)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to download archive: %v", err)})
			return
		}
		defer os.Remove(tmpPath)
	} else {
		if len(source.files) != 1 {
			c.JSON(http.StatusBadRequest, gin.H{"error": "batch archive import accepts exactly one uploaded file"})
			return
		}
		var err error
		upload := source.files[0]
		tmpPath, err = saveSkillUploadToTemp(upload.file, upload.filename, "agentteam-skill-batch-*")
		if err != nil {
			writeSkillUploadError(c, err)
			return
		}
		defer removeSkillUploadTemp(tmpPath)
	}

	result, err := profile.Default().Skills().ImportBatchFromFile(tmpPath, skillImportForceFromForm(c))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Convert profile types to DTO types.
	resp := dto.BatchImportResponse{
		Imported: make([]dto.BatchImportResult, len(result.Imported)),
		Created:  result.Created,
		Skipped:  result.Skipped,
		Errors:   result.Errors,
	}
	for i, r := range result.Imported {
		resp.Imported[i] = dto.BatchImportResult{
			ID:     r.ID,
			Name:   r.Name,
			Status: r.Status,
		}
	}

	c.JSON(http.StatusCreated, resp)
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

func skillFilesRoot(id string) (string, error) {
	skillsRoot, err := filepath.Abs(profile.Default().Skills().Dir())
	if err != nil {
		return "", err
	}
	root, err := filepath.Abs(filepath.Join(skillsRoot, id))
	if err != nil {
		return "", err
	}
	rel, err := filepath.Rel(skillsRoot, root)
	if err != nil {
		return "", err
	}
	if rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
		return "", fmt.Errorf("invalid skill files path")
	}
	return root, nil
}

func listSkillFileNodes(root, rel string, count *int) ([]dto.SkillFileNode, bool, error) {
	if *count >= maxSkillFileEntries {
		return []dto.SkillFileNode{}, true, nil
	}
	dir, err := safeSkillFilePath(root, rel)
	if err != nil {
		return nil, false, err
	}

	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, false, 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.SkillFileNode, 0, len(entries))
	truncated := false
	for _, entry := range entries {
		if *count >= maxSkillFileEntries {
			truncated = true
			break
		}
		name := entry.Name()
		path := filepath.ToSlash(filepath.Join(rel, name))
		info, err := entry.Info()
		if err != nil {
			continue
		}

		*count++
		node := dto.SkillFileNode{
			Name:       name,
			Path:       path,
			IsDir:      entry.IsDir(),
			ModifiedAt: info.ModTime().UTC().Format(time.RFC3339),
		}
		if entry.IsDir() {
			children, childTruncated, err := listSkillFileNodes(root, filepath.Join(rel, name), count)
			if err != nil {
				continue
			}
			node.Children = children
			truncated = truncated || childTruncated
		} else {
			node.Size = info.Size()
		}
		nodes = append(nodes, node)
	}
	return nodes, truncated, nil
}

func safeSkillFilePath(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
}

func buildSkillResponse(s profile.Skill) dto.SkillResponse {
	resp := dto.SkillResponse{
		ID:          s.ID,
		Name:        s.Name,
		Description: s.Description,
		Version:     s.Version,
		Author:      s.Author,
		Tags:        s.Tags,
		FileName:    s.FileName,
	}
	if s.CreatedAt != nil {
		t := s.CreatedAt.UTC().Format(time.RFC3339)
		resp.CreatedAt = &t
	}
	return resp
}

func splitTags(s string) []string {
	var tags []string
	for _, t := range strings.Split(s, ",") {
		t = strings.TrimSpace(t)
		if t != "" {
			tags = append(tags, t)
		}
	}
	return tags
}
