package main

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
)

const (
	fsDefaultMaxBytes = 64 * 1024
	fsHardMaxBytes    = 1024 * 1024
)

// isPathAllowed valida path contra una lista de globs declarados en cap.PathsAllowed.
// Aplica EvalSymlinks (best-effort) y filepath.Clean para neutralizar ../ + symlinks.
// Soporta el sufijo "/**" para match recursivo bajo un prefijo.
func isPathAllowed(path string, allowed []string) bool {
	if len(allowed) == 0 {
		return false
	}
	abs, err := filepath.Abs(path)
	if err != nil {
		return false
	}
	abs = filepath.Clean(abs)

	// Resolve symlinks si el target existe; si no existe (write a archivo nuevo)
	// resolvemos el directorio padre, no romper.
	resolved := abs
	if real, err := filepath.EvalSymlinks(abs); err == nil {
		resolved = real
	} else {
		// Para paths no existentes (write): intenta resolver el dir padre.
		dir := filepath.Dir(abs)
		base := filepath.Base(abs)
		if realDir, err := filepath.EvalSymlinks(dir); err == nil {
			resolved = filepath.Join(realDir, base)
		}
	}
	resolved = filepath.Clean(resolved)

	for _, pat := range allowed {
		patClean := filepath.Clean(pat)
		// Soporte sufijo /** recursivo
		if strings.HasSuffix(patClean, string(filepath.Separator)+"**") || strings.HasSuffix(patClean, "/**") {
			prefix := strings.TrimSuffix(patClean, "/**")
			prefix = strings.TrimSuffix(prefix, string(filepath.Separator)+"**")
			prefix = filepath.Clean(prefix)
			if resolved == prefix || strings.HasPrefix(resolved, prefix+string(filepath.Separator)) {
				return true
			}
			continue
		}
		// Si patron es un dir y resolved esta debajo, allow (caso /etc/)
		if strings.HasSuffix(pat, "/") {
			prefix := filepath.Clean(pat)
			if strings.HasPrefix(resolved, prefix+string(filepath.Separator)) || resolved == prefix {
				return true
			}
		}
		// Match exacto o glob simple
		if match, _ := filepath.Match(patClean, resolved); match {
			return true
		}
		if resolved == patClean {
			return true
		}
	}
	return false
}

// mapIntField extrae un int de map[string]any tolerando float64 (JSON default).
func mapIntField(m map[string]any, key string, def int) int {
	if v, ok := m[key]; ok && v != nil {
		switch t := v.(type) {
		case float64:
			return int(t)
		case int:
			return t
		case int64:
			return int(t)
		case json.Number:
			n, err := t.Int64()
			if err == nil {
				return int(n)
			}
		}
	}
	return def
}

func mapStringField(m map[string]any, key string) string {
	if v, ok := m[key]; ok && v != nil {
		if s, ok := v.(string); ok {
			return s
		}
	}
	return ""
}

// runFsRead lee un archivo y devuelve content_b64 + meta. Trunca a max_bytes.
func runFsRead(cap *Capability, args map[string]any) (any, int, error) {
	path := mapStringField(args, "path")
	if path == "" {
		return nil, -1, fmt.Errorf("path required")
	}
	maxBytes := mapIntField(args, "max_bytes", fsDefaultMaxBytes)
	if maxBytes <= 0 {
		maxBytes = fsDefaultMaxBytes
	}
	if maxBytes > fsHardMaxBytes {
		maxBytes = fsHardMaxBytes
	}
	if !isPathAllowed(path, cap.PathsAllowed) {
		return nil, -1, fmt.Errorf("path not allowed by manifest: %s", path)
	}
	st, err := os.Stat(path)
	if err != nil {
		return nil, -1, fmt.Errorf("stat: %w", err)
	}
	if st.IsDir() {
		return nil, -1, fmt.Errorf("path is a directory")
	}
	f, err := os.Open(path) // #nosec G304 — whitelisted above
	if err != nil {
		return nil, -1, fmt.Errorf("open: %w", err)
	}
	defer f.Close()
	buf := make([]byte, maxBytes)
	n, err := f.Read(buf)
	if err != nil && err.Error() != "EOF" && n == 0 {
		return nil, -1, fmt.Errorf("read: %w", err)
	}
	truncated := int64(n) < st.Size()
	return map[string]any{
		"content_b64": base64.StdEncoding.EncodeToString(buf[:n]),
		"size":        st.Size(),
		"bytes_read":  n,
		"mtime":       st.ModTime().Unix(),
		"truncated":   truncated,
		"path":        path,
	}, 0, nil
}

// runFsWrite escribe content_b64 a path. Mkdir parent, default mode 0644.
func runFsWrite(cap *Capability, args map[string]any) (any, int, error) {
	path := mapStringField(args, "path")
	if path == "" {
		return nil, -1, fmt.Errorf("path required")
	}
	contentB64 := mapStringField(args, "content_b64")
	if contentB64 == "" {
		// Soporte fallback "content" plano
		if c := mapStringField(args, "content"); c != "" {
			contentB64 = base64.StdEncoding.EncodeToString([]byte(c))
		}
	}
	mode := mapIntField(args, "mode", 0644)
	if !isPathAllowed(path, cap.PathsAllowed) {
		return nil, -1, fmt.Errorf("path not allowed by manifest: %s", path)
	}
	data, err := base64.StdEncoding.DecodeString(contentB64)
	if err != nil {
		return nil, -1, fmt.Errorf("invalid content_b64: %w", err)
	}
	if int64(len(data)) > fsHardMaxBytes {
		return nil, -1, fmt.Errorf("content too large (>1MB)")
	}
	parent := filepath.Dir(path)
	if err := os.MkdirAll(parent, 0755); err != nil {
		return nil, -1, fmt.Errorf("mkdir parent: %w", err)
	}
	if err := os.WriteFile(path, data, os.FileMode(mode)); err != nil { // #nosec G306
		return nil, -1, fmt.Errorf("write: %w", err)
	}
	return map[string]any{
		"path":          path,
		"bytes_written": len(data),
	}, 0, nil
}

// runFsList lista un directorio (no recursivo). glob opcional filtra entries.
func runFsList(cap *Capability, args map[string]any) (any, int, error) {
	dir := mapStringField(args, "dir")
	if dir == "" {
		return nil, -1, fmt.Errorf("dir required")
	}
	glob := mapStringField(args, "glob")
	if !isPathAllowed(dir, cap.PathsAllowed) {
		return nil, -1, fmt.Errorf("dir not allowed by manifest: %s", dir)
	}
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, -1, fmt.Errorf("readdir: %w", err)
	}
	out := []map[string]any{}
	for _, e := range entries {
		if glob != "" {
			if m, _ := filepath.Match(glob, e.Name()); !m {
				continue
			}
		}
		info, ierr := e.Info()
		kind := "file"
		var size int64
		var mtime int64
		if ierr == nil {
			if info.Mode()&os.ModeSymlink != 0 {
				kind = "symlink"
			} else if info.IsDir() {
				kind = "dir"
			}
			size = info.Size()
			mtime = info.ModTime().Unix()
		}
		out = append(out, map[string]any{
			"name":  e.Name(),
			"kind":  kind,
			"size":  size,
			"mtime": mtime,
		})
	}
	sort.Slice(out, func(i, j int) bool {
		return out[i]["name"].(string) < out[j]["name"].(string)
	})
	return map[string]any{
		"dir":     dir,
		"entries": out,
		"count":   len(out),
	}, 0, nil
}

// runFsStat devuelve metadata de un archivo o directorio.
func runFsStat(cap *Capability, args map[string]any) (any, int, error) {
	path := mapStringField(args, "path")
	if path == "" {
		return nil, -1, fmt.Errorf("path required")
	}
	if !isPathAllowed(path, cap.PathsAllowed) {
		return nil, -1, fmt.Errorf("path not allowed by manifest: %s", path)
	}
	st, err := os.Lstat(path)
	if err != nil {
		return nil, -1, fmt.Errorf("stat: %w", err)
	}
	kind := "file"
	if st.IsDir() {
		kind = "dir"
	} else if st.Mode()&os.ModeSymlink != 0 {
		kind = "symlink"
	}
	res := map[string]any{
		"path":  path,
		"kind":  kind,
		"size":  st.Size(),
		"mode":  fmt.Sprintf("%#o", st.Mode().Perm()),
		"mtime": st.ModTime().Unix(),
	}
	return res, 0, nil
}
