package tools

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"log/slog"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"

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

// Dangerous command patterns organized into configurable deny groups.
// Defense-in-depth: patterns complement Docker hardening (cap-drop ALL,
// no-new-privileges, pids-limit, memory limit).
// Sources: OWASP Agentic AI Top 10, Claude Code CVE-2025-66032, MITRE ATT&CK,
// PayloadsAllTheThings, Trail of Bits prompt-injection-to-RCE research.
// Groups and patterns defined in shell_deny_groups.go.

// DefaultDenyPatterns returns all patterns from groups where Default=true.
// Backward-compatible wrapper for code that doesn't use per-agent overrides.
func DefaultDenyPatterns() []*regexp.Regexp {
	return ResolveDenyPatterns(nil)
}

// ExecTool executes shell commands, optionally inside a sandbox container.
type ExecTool struct {
	workingDir       string
	timeout          time.Duration
	pathDenyPatterns []*regexp.Regexp     // always-on path-based denials (DenyPaths)
	denyExemptions   []string             // substrings that exempt a command from deny
	restrict         bool
	sandboxMgr       sandbox.Manager      // nil = no sandbox, execute on host
	approvalMgr      *ExecApprovalManager // nil = no approval needed
	agentID          string               // for approval request context
	secureCLIStore   store.SecureCLIStore  // nil = no credentialed exec
}

// NewExecTool creates an exec tool that runs commands directly on the host.
func NewExecTool(workingDir string, restrict bool) *ExecTool {
	return &ExecTool{
		workingDir: workingDir,
		timeout:    60 * time.Second,
		restrict:   restrict,
	}
}

// NewSandboxedExecTool creates an exec tool that routes commands through a sandbox container.
func NewSandboxedExecTool(workingDir string, restrict bool, mgr sandbox.Manager) *ExecTool {
	return &ExecTool{
		workingDir: workingDir,
		timeout:    300 * time.Second, // sandbox allows longer timeout
		restrict:   restrict,
		sandboxMgr: mgr,
	}
}

// SetSandboxKey is a no-op; sandbox key is now read from ctx (thread-safe).
func (t *ExecTool) SetSandboxKey(key string) {}

// DenyPaths adds always-on deny patterns that block commands referencing the given paths.
// These are NOT configurable via deny groups — they always apply regardless of group config.
func (t *ExecTool) DenyPaths(paths ...string) {
	for _, p := range paths {
		escaped := regexp.QuoteMeta(p)
		t.pathDenyPatterns = append(t.pathDenyPatterns, regexp.MustCompile(escaped))
	}
}

// AllowPathExemptions adds substrings that exempt a command from deny pattern matches.
func (t *ExecTool) AllowPathExemptions(substrings ...string) {
	t.denyExemptions = append(t.denyExemptions, substrings...)
}

// SetApprovalManager sets the exec approval manager for this tool.
func (t *ExecTool) SetApprovalManager(mgr *ExecApprovalManager, agentID string) {
	t.approvalMgr = mgr
	t.agentID = agentID
}

// SetSecureCLIStore sets the credential store for credentialed exec.
func (t *ExecTool) SetSecureCLIStore(s store.SecureCLIStore) {
	t.secureCLIStore = s
}

func (t *ExecTool) Name() string        { return "exec" }
func (t *ExecTool) Description() string { return "Execute a shell command and return its output" }
func (t *ExecTool) Parameters() map[string]any {
	return map[string]any{
		"type": "object",
		"properties": map[string]any{
			"command": map[string]any{
				"type":        "string",
				"description": "The shell command to execute",
			},
			"working_dir": map[string]any{
				"type":        "string",
				"description": "Working directory for the command (default: workspace root)",
			},
		},
		"required": []string{"command"},
	}
}

func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *Result {
	command, _ := args["command"].(string)
	if command == "" {
		return ErrorResult("command is required")
	}

	// Resolve deny patterns: per-agent overrides from context, fallback to all defaults.
	denyOverrides := store.ShellDenyGroupsFromContext(ctx)
	groupPatterns := ResolveDenyPatterns(denyOverrides)

	// Also resolve package_install patterns separately for approval routing.
	var pkgInstallPatterns []*regexp.Regexp
	if pkgGroup, ok := DenyGroupRegistry["package_install"]; ok && IsGroupDenied(denyOverrides, "package_install") {
		pkgInstallPatterns = pkgGroup.Patterns
	}

	// Combine group-based patterns + always-on path denials.
	allPatterns := make([]*regexp.Regexp, 0, len(groupPatterns)+len(t.pathDenyPatterns))
	allPatterns = append(allPatterns, groupPatterns...)
	allPatterns = append(allPatterns, t.pathDenyPatterns...)

	// Check for dangerous commands (applies to both host and sandbox).
	for _, pattern := range allPatterns {
		if pattern.MatchString(command) {
			// Check if any exemption applies (e.g. skills-store within .goclaw)
			exempt := false
			for _, ex := range t.denyExemptions {
				if strings.Contains(command, ex) {
					exempt = true
					break
				}
			}
			if exempt {
				continue
			}

			// Package install commands: route through approval flow instead of hard deny.
			// This lets agents "request permission" from admin to install packages.
			if t.approvalMgr != nil && matchesAny(command, pkgInstallPatterns) {
				slog.Info("exec: package install requires approval", "command", truncateCmd(command, 100), "agent", t.agentID)
				decision, err := t.approvalMgr.RequestApproval(command, t.agentID, 2*time.Minute)
				if err != nil {
					return ErrorResult(fmt.Sprintf("package install approval: %v", err))
				}
				if decision == ApprovalDeny {
					return ErrorResult("package installation denied by admin")
				}
				// Approved — skip deny, continue to execution.
				continue
			}

			return ErrorResult(fmt.Sprintf("command denied by safety policy: matches pattern %s", pattern.String()))
		}
	}

	// Credentialed exec: if command matches a configured binary, use Direct Exec Mode.
	// This bypasses approval (admin trust) and shell (security).
	if cred, binary, cmdArgs := t.lookupCredentialedBinary(ctx, command); cred != nil {
		cwd := ToolWorkspaceFromCtx(ctx)
		if cwd == "" {
			cwd = t.workingDir
		}
		if wd, _ := args["working_dir"].(string); wd != "" {
			if effectiveRestrict(ctx, t.restrict) {
				if resolved, err := resolvePath(wd, t.workingDir, true); err == nil {
					cwd = resolved
				}
			} else {
				cwd = wd
			}
		}
		sandboxKey := ToolSandboxKeyFromCtx(ctx)
		return t.executeCredentialed(ctx, cred, binary, cmdArgs, cwd, sandboxKey)
	}

	// Exec approval check (matching TS exec-approval.ts pipeline)
	if t.approvalMgr != nil {
		switch t.approvalMgr.CheckCommand(command) {
		case "deny":
			return ErrorResult("command denied by exec approval policy")
		case "ask":
			decision, err := t.approvalMgr.RequestApproval(command, t.agentID, 2*time.Minute)
			if err != nil {
				return ErrorResult(fmt.Sprintf("exec approval: %v", err))
			}
			if decision == ApprovalDeny {
				return ErrorResult("command denied by user")
			}
		}
	}

	// Use per-user workspace from context if available, fallback to struct field
	cwd := ToolWorkspaceFromCtx(ctx)
	if cwd == "" {
		cwd = t.workingDir
	}
	if wd, _ := args["working_dir"].(string); wd != "" {
		if effectiveRestrict(ctx, t.restrict) {
			resolved, err := resolvePath(wd, t.workingDir, true)
			if err != nil {
				return ErrorResult(err.Error())
			}
			cwd = resolved
		} else {
			cwd = wd
		}
	}

	// Sandbox routing (sandboxKey from ctx — thread-safe)
	sandboxKey := ToolSandboxKeyFromCtx(ctx)
	if t.sandboxMgr != nil && sandboxKey != "" {
		return t.executeInSandbox(ctx, command, cwd, sandboxKey)
	}

	// Host execution
	return t.executeOnHost(ctx, command, cwd)
}

// matchesAny checks if a command matches any pattern in the list.
func matchesAny(command string, patterns []*regexp.Regexp) bool {
	for _, p := range patterns {
		if p.MatchString(command) {
			return true
		}
	}
	return false
}

// executeOnHost runs a command directly on the host (original behavior).
func (t *ExecTool) executeOnHost(ctx context.Context, command, cwd string) *Result {
	ctx, cancel := context.WithTimeout(ctx, t.timeout)
	defer cancel()

	cmd := exec.CommandContext(ctx, "sh", "-c", command)
	cmd.Dir = cwd

	// Limit output to 1MB to prevent OOM from runaway commands.
	stdout := &limitedBuffer{max: 1 << 20}
	stderr := &limitedBuffer{max: 1 << 20}
	cmd.Stdout = stdout
	cmd.Stderr = stderr

	err := cmd.Run()

	var result string
	if stdout.Len() > 0 {
		result = stdout.String()
	}
	if stderr.Len() > 0 {
		if result != "" {
			result += "\n"
		}
		result += "STDERR:\n" + stderr.String()
	}

	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			return ErrorResult(fmt.Sprintf("command timed out after %s", t.timeout))
		}
		if result == "" {
			result = err.Error()
		}
		return ErrorResult(result)
	}

	if result == "" {
		result = "(command completed with no output)"
	}

	return SilentResult(result)
}

// executeInSandbox routes a command through a Docker sandbox container.
func (t *ExecTool) executeInSandbox(ctx context.Context, command, cwd, sandboxKey string) *Result {
	sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workingDir, SandboxConfigFromCtx(ctx))
	if err != nil {
		if errors.Is(err, sandbox.ErrSandboxDisabled) {
			return t.executeOnHost(ctx, command, cwd)
		}
		// Docker unavailable (binary missing, daemon down) → fail closed.
		// Do NOT silently fallback to host — that defeats the purpose of sandboxing.
		slog.Warn("security.sandbox_unavailable",
			"error", err,
			"command", truncateCmd(command, 80),
		)
		return ErrorResult(fmt.Sprintf("sandbox unavailable: %v (will not fall back to unsandboxed host execution)", err))
	}

	// Map host workdir to container workdir
	containerCwd := "/workspace"
	if cwd != t.workingDir {
		rel, relErr := filepath.Rel(t.workingDir, cwd)
		if relErr == nil {
			containerCwd = filepath.Join("/workspace", rel)
		}
	}

	result, err := sb.Exec(ctx, []string{"sh", "-c", command}, containerCwd) //nolint: no ExecOption for normal exec
	if err != nil {
		return ErrorResult(fmt.Sprintf("sandbox exec: %v", err))
	}

	// Format output same as host execution
	output := result.Stdout
	if result.Stderr != "" {
		if output != "" {
			output += "\n"
		}
		output += "STDERR:\n" + result.Stderr
	}
	if result.ExitCode != 0 {
		if output == "" {
			output = fmt.Sprintf("command exited with code %d", result.ExitCode)
		}
		return ErrorResult(output)
	}
	if output == "" {
		output = "(command completed with no output)"
	}

	return SilentResult(output)
}

// limitedBuffer caps output to prevent OOM from runaway commands.
type limitedBuffer struct {
	buf       bytes.Buffer
	max       int
	truncated bool
}

func (lb *limitedBuffer) Write(p []byte) (int, error) {
	if lb.truncated {
		return len(p), nil
	}
	remaining := lb.max - lb.buf.Len()
	if remaining <= 0 {
		lb.truncated = true
		return len(p), nil
	}
	if len(p) > remaining {
		lb.buf.Write(p[:remaining])
		lb.truncated = true
		return len(p), nil
	}
	return lb.buf.Write(p)
}

func (lb *limitedBuffer) String() string {
	s := lb.buf.String()
	if lb.truncated {
		s += "\n[output truncated at 1MB]"
	}
	return s
}

func (lb *limitedBuffer) Len() int { return lb.buf.Len() }
