package tools

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

	"github.com/nextlevelbuilder/goclaw/internal/sandbox"
	"github.com/nextlevelbuilder/goclaw/internal/store"
)

// EditTool performs search-and-replace edits on files.
// Supports context file interceptor and sandbox routing.
type EditTool struct {
	workspace        string
	restrict         bool
	deniedPrefixes   []string // path prefixes to deny access to (e.g. .goclaw)
	sandboxMgr       sandbox.Manager
	contextFileIntc  *ContextFileInterceptor
	memIntc          *MemoryInterceptor
	groupWriterCache *store.GroupWriterCache // nil = no group write restriction
}

// DenyPaths adds path prefixes that edit must reject.
func (t *EditTool) DenyPaths(prefixes ...string) {
	t.deniedPrefixes = append(t.deniedPrefixes, prefixes...)
}

func (t *EditTool) SetContextFileInterceptor(intc *ContextFileInterceptor) {
	t.contextFileIntc = intc
}

func (t *EditTool) SetMemoryInterceptor(intc *MemoryInterceptor) {
	t.memIntc = intc
}

// SetGroupWriterCache enables group write permission checks.
func (t *EditTool) SetGroupWriterCache(c *store.GroupWriterCache) {
	t.groupWriterCache = c
}

func NewEditTool(workspace string, restrict bool) *EditTool {
	return &EditTool{workspace: workspace, restrict: restrict}
}

func NewSandboxedEditTool(workspace string, restrict bool, mgr sandbox.Manager) *EditTool {
	return &EditTool{workspace: workspace, restrict: restrict, sandboxMgr: mgr}
}

func (t *EditTool) SetSandboxKey(key string) {}

func (t *EditTool) Name() string { return "edit" }
func (t *EditTool) Description() string {
	return "Edit a file by replacing exact text matches. Use old_string/new_string for precise edits without rewriting the entire file."
}

func (t *EditTool) Parameters() map[string]any {
	return map[string]any{
		"type": "object",
		"properties": map[string]any{
			"path": map[string]any{
				"type":        "string",
				"description": "File path (relative to workspace, or absolute)",
			},
			"old_string": map[string]any{
				"type":        "string",
				"description": "Exact text to find (must match uniquely unless replace_all is true)",
			},
			"new_string": map[string]any{
				"type":        "string",
				"description": "Replacement text",
			},
			"replace_all": map[string]any{
				"type":        "boolean",
				"description": "Replace all occurrences (default: false, requires unique match)",
			},
		},
		"required": []string{"path", "old_string", "new_string"},
	}
}

func (t *EditTool) Execute(ctx context.Context, args map[string]any) *Result {
	path, _ := args["path"].(string)
	oldStr, _ := args["old_string"].(string)
	newStr, _ := args["new_string"].(string)
	replaceAll, _ := args["replace_all"].(bool)

	if path == "" {
		return ErrorResult("path is required")
	}
	if oldStr == "" {
		return ErrorResult("old_string is required")
	}
	if oldStr == newStr {
		return ErrorResult("old_string and new_string are identical")
	}

	// Group write permission check
	if t.groupWriterCache != nil {
		if err := store.CheckGroupWritePermission(ctx, t.groupWriterCache); err != nil {
			return ErrorResult(err.Error())
		}
	}

	// Virtual FS: context files
	if t.contextFileIntc != nil {
		if content, handled, err := t.contextFileIntc.ReadFile(ctx, path); handled {
			if err != nil {
				return ErrorResult(fmt.Sprintf("failed to read context file: %v", err))
			}
			if content == "" {
				return ErrorResult(fmt.Sprintf("context file not found: %s", path))
			}
			newContent, result := applyEdit(content, oldStr, newStr, replaceAll)
			if result != nil {
				return result
			}
			if _, err := t.contextFileIntc.WriteFile(ctx, path, newContent); err != nil {
				return ErrorResult(fmt.Sprintf("failed to write context file: %v", err))
			}
			return SilentResult(fmt.Sprintf("Context file edited: %s", path))
		}
	}

	// Virtual FS: memory files
	if t.memIntc != nil {
		if content, handled, err := t.memIntc.ReadFile(ctx, path); handled {
			if err != nil {
				return ErrorResult(fmt.Sprintf("failed to read memory file: %v", err))
			}
			if content == "" {
				return ErrorResult(fmt.Sprintf("memory file not found: %s", path))
			}
			newContent, result := applyEdit(content, oldStr, newStr, replaceAll)
			if result != nil {
				return result
			}
			mwr, err := t.memIntc.WriteFile(ctx, path, newContent)
			if err != nil {
				return ErrorResult(fmt.Sprintf("failed to write memory file: %v", err))
			}
			msg := fmt.Sprintf("Memory file edited: %s", path)
			if mwr.KGTriggered {
				msg += "\n\n[Knowledge graph extraction triggered in background. The knowledge system may take a moment to fully update with new entities and relationships.]"
			}
			return SilentResult(msg)
		}
	}

	// Sandbox routing
	sandboxKey := ToolSandboxKeyFromCtx(ctx)
	if t.sandboxMgr != nil && sandboxKey != "" {
		return t.executeInSandbox(ctx, path, oldStr, newStr, replaceAll, sandboxKey)
	}

	// Host execution — use per-user workspace from context if available
	workspace := ToolWorkspaceFromCtx(ctx)
	if workspace == "" {
		workspace = t.workspace
	}
	allowed := allowedWithTeamWorkspace(ctx, nil)
	resolved, err := resolvePathWithAllowed(path, workspace, effectiveRestrict(ctx, t.restrict), allowed)
	if err != nil {
		return ErrorResult(err.Error())
	}
	if err := checkDeniedPath(resolved, t.workspace, t.deniedPrefixes); err != nil {
		return ErrorResult(err.Error())
	}

	data, err := os.ReadFile(resolved)
	if err != nil {
		return ErrorResult(fmt.Sprintf("failed to read file: %v", err))
	}

	content := string(data)
	newContent, result := applyEdit(content, oldStr, newStr, replaceAll)
	if result != nil {
		return result
	}

	if err := os.MkdirAll(filepath.Dir(resolved), 0755); err != nil {
		return ErrorResult(fmt.Sprintf("failed to create directory: %v", err))
	}

	if err := os.WriteFile(resolved, []byte(newContent), 0644); err != nil {
		return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
	}

	count := strings.Count(content, oldStr)
	return SilentResult(fmt.Sprintf("File edited: %s (%d replacement(s))", path, count))
}

func (t *EditTool) executeInSandbox(ctx context.Context, path, oldStr, newStr string, replaceAll bool, sandboxKey string) *Result {
	sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workspace, SandboxConfigFromCtx(ctx))
	if err != nil {
		return ErrorResult(fmt.Sprintf("sandbox error: %v", err))
	}

	bridge := sandbox.NewFsBridge(sb.ID(), "/workspace")
	content, err := bridge.ReadFile(ctx, path)
	if err != nil {
		return ErrorResult(fmt.Sprintf("failed to read file: %v", err))
	}

	newContent, result := applyEdit(content, oldStr, newStr, replaceAll)
	if result != nil {
		return result
	}

	if err := bridge.WriteFile(ctx, path, newContent); err != nil {
		return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
	}

	count := strings.Count(content, oldStr)
	return SilentResult(fmt.Sprintf("File edited: %s (%d replacement(s))", path, count))
}

// applyEdit performs the search-and-replace. Returns (newContent, nil) on success
// or ("", errorResult) on failure.
func applyEdit(content, oldStr, newStr string, replaceAll bool) (string, *Result) {
	count := strings.Count(content, oldStr)
	if count == 0 {
		return "", ErrorResult("old_string not found in file")
	}
	if !replaceAll && count > 1 {
		return "", ErrorResult(fmt.Sprintf("old_string found %d times — use replace_all=true or provide a more specific match", count))
	}

	if replaceAll {
		return strings.ReplaceAll(content, oldStr, newStr), nil
	}
	return strings.Replace(content, oldStr, newStr, 1), nil
}
