package tools

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"time"

	"github.com/google/uuid"
	shellwords "github.com/mattn/go-shellwords"

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

// shellOperatorPattern detects shell metacharacters that indicate command chaining.
// These are unsafe in credentialed mode because they allow reading injected env vars.
var shellOperatorPattern = regexp.MustCompile(`[;|&<>\n\r` + "`" + `]|\$\(|\$\{`)

// parseCommandBinary splits a command string into binary name and arguments.
// Uses shell-word parsing to correctly handle quoted arguments with spaces.
func parseCommandBinary(command string) (binary string, args []string, err error) {
	parser := shellwords.NewParser()
	parser.ParseBacktick = false
	parser.ParseEnv = false

	words, err := parser.Parse(command)
	if err != nil {
		return "", nil, fmt.Errorf("parse command: %w", err)
	}
	if len(words) == 0 {
		return "", nil, fmt.Errorf("empty command")
	}
	return words[0], words[1:], nil
}

// detectShellOperators scans a raw command string for shell metacharacters.
// Returns the list of detected operators, or nil if the command is clean.
func detectShellOperators(command string) []string {
	matches := shellOperatorPattern.FindAllString(command, -1)
	if len(matches) == 0 {
		return nil
	}
	// Deduplicate
	seen := make(map[string]bool, len(matches))
	var unique []string
	for _, m := range matches {
		if !seen[m] {
			seen[m] = true
			unique = append(unique, m)
		}
	}
	return unique
}

// resolveAndMatchBinary resolves a binary name to an absolute path and
// optionally verifies it matches the stored config path. This prevents
// binary spoofing (e.g. ./gh in workspace instead of /usr/bin/gh).
func resolveAndMatchBinary(binaryName string, configPath *string) (string, error) {
	absPath, err := exec.LookPath(binaryName)
	if err != nil {
		return "", fmt.Errorf("binary %q not found in PATH: %w", binaryName, err)
	}
	// If config specifies an absolute path, verify it matches
	if configPath != nil && *configPath != "" && absPath != *configPath {
		return "", fmt.Errorf("binary path mismatch: resolved %q but config expects %q", absPath, *configPath)
	}
	return absPath, nil
}

// matchesBinaryDeny checks if the joined args string matches any per-binary deny pattern.
// Returns the matched pattern string, or empty if allowed.
func matchesBinaryDeny(args []string, denyPatternsJSON json.RawMessage) string {
	if len(denyPatternsJSON) == 0 {
		return ""
	}
	var patterns []string
	if err := json.Unmarshal(denyPatternsJSON, &patterns); err != nil || len(patterns) == 0 {
		return ""
	}
	argsStr := strings.Join(args, " ")
	for _, p := range patterns {
		re, err := regexp.Compile(p)
		if err != nil {
			slog.Warn("secure_cli.invalid_deny_pattern", "pattern", p, "error", err)
			continue
		}
		if re.MatchString(argsStr) {
			return p
		}
	}
	return ""
}

// executeCredentialed runs a CLI command in Direct Exec Mode (no shell).
// Credentials are injected as env vars into the child process only.
func (t *ExecTool) executeCredentialed(ctx context.Context, cred *store.SecureCLIBinary,
	binary string, args []string, cwd string, sandboxKey string) *Result {

	// Step 1: Check for shell operators (early detection for clear error)
	rawCommand := binary + " " + strings.Join(args, " ")
	if ops := detectShellOperators(rawCommand); len(ops) > 0 {
		return credentialedShellOperatorError(rawCommand, ops)
	}

	// Step 2: Resolve binary to absolute path and verify against config
	absPath, err := resolveAndMatchBinary(binary, cred.BinaryPath)
	if err != nil {
		return credentialedPathError(binary, err)
	}

	// Step 3: Per-binary deny check (deny_args)
	if p := matchesBinaryDeny(args, cred.DenyArgs); p != "" {
		return credentialedDenyError(binary, args, p)
	}
	// Per-binary verbose deny check (deny_verbose)
	if p := matchesBinaryDeny(args, cred.DenyVerbose); p != "" {
		return credentialedDenyError(binary, args, p)
	}

	// Step 4: Decrypt env vars from store (already decrypted by store layer)
	envMap := make(map[string]string)
	if len(cred.EncryptedEnv) > 0 {
		if err := json.Unmarshal(cred.EncryptedEnv, &envMap); err != nil {
			return ErrorResult(fmt.Sprintf("credentialed exec: invalid env JSON for %q: %v", binary, err))
		}
	}

	// Step 5: Register credential values for output scrubbing
	for _, v := range envMap {
		AddCredentialScrubValues(v)
	}

	// Step 6: Determine timeout
	timeout := time.Duration(cred.TimeoutSeconds) * time.Second
	if timeout <= 0 {
		timeout = 30 * time.Second
	}

	// Step 7: Execute — sandbox or host
	if t.sandboxMgr != nil && sandboxKey != "" {
		return t.executeCredentialedSandbox(ctx, absPath, args, cwd, sandboxKey, envMap, timeout)
	}
	return t.executeCredentialedHost(ctx, absPath, args, cwd, envMap, timeout)
}

// executeCredentialedHost runs a credentialed command directly on the host.
// Uses exec.Command (no shell) with credentials as env vars.
func (t *ExecTool) executeCredentialedHost(ctx context.Context, absPath string, args []string,
	cwd string, envMap map[string]string, timeout time.Duration) *Result {

	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	cmd := exec.CommandContext(ctx, absPath, args...)
	cmd.Dir = cwd

	// Build env: inherit minimal PATH + HOME, add credentials
	cmd.Env = buildCredentialedEnv(envMap)

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	return formatCredentialedResult(absPath, args, stdout.String(), stderr.String(), err, ctx, timeout)
}

// executeCredentialedSandbox runs a credentialed command inside a Docker sandbox.
// Uses sandbox.WithEnv to inject credentials via docker exec -e (no shell).
func (t *ExecTool) executeCredentialedSandbox(ctx context.Context, absPath string, args []string,
	cwd string, sandboxKey string, envMap map[string]string, timeout time.Duration) *Result {

	sb, err := t.sandboxMgr.Get(ctx, sandboxKey, t.workingDir, SandboxConfigFromCtx(ctx))
	if err != nil {
		slog.Warn("security.credentialed_exec_sandbox_unavailable",
			"binary", absPath, "error", err)
		return ErrorResult("credentialed exec requires sandbox but sandbox is unavailable: " + err.Error())
	}

	// Direct exec inside sandbox: [absPath, args...] with env injection
	command := append([]string{absPath}, args...)
	result, err := sb.Exec(ctx, command, cwd, sandbox.WithEnv(envMap))
	if err != nil {
		return ErrorResult(fmt.Sprintf("credentialed sandbox exec: %v", err))
	}

	output := result.Stdout
	if result.Stderr != "" {
		if output != "" {
			output += "\n"
		}
		output += "STDERR:\n" + result.Stderr
	}
	if result.ExitCode != 0 {
		return credentialedExecFailError(absPath, args, result.ExitCode, ScrubCredentials(output))
	}
	if output == "" {
		output = "(command completed with no output)"
	}
	return SilentResult(ScrubCredentials(output))
}

// buildCredentialedEnv creates a minimal environment with injected credentials.
// Inherits PATH and HOME from parent process, adds credential env vars.
func buildCredentialedEnv(envMap map[string]string) []string {
	env := []string{
		"PATH=" + getenvDefault("PATH", "/usr/local/bin:/usr/bin:/bin"),
		"HOME=" + getenvDefault("HOME", "/tmp"),
		"LANG=" + getenvDefault("LANG", "en_US.UTF-8"),
		"USER=" + getenvDefault("USER", "goclaw"),
	}
	for k, v := range envMap {
		env = append(env, k+"="+v)
	}
	return env
}

// formatCredentialedResult formats the output of a credentialed exec call.
func formatCredentialedResult(binary string, args []string,
	stdout, stderr string, err error, ctx context.Context, timeout time.Duration) *Result {

	var output string
	if stdout != "" {
		output = stdout
	}
	if stderr != "" {
		if output != "" {
			output += "\n"
		}
		output += "STDERR:\n" + stderr
	}

	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			return ErrorResult(fmt.Sprintf("[CREDENTIALED EXEC] Command timed out after %s.\nBinary: %s", timeout, binary))
		}
		exitCode := -1
		if exitErr, ok := err.(*exec.ExitError); ok {
			exitCode = exitErr.ExitCode()
		}
		return credentialedExecFailError(binary, args, exitCode, ScrubCredentials(output))
	}

	if output == "" {
		output = "(command completed with no output)"
	}
	return SilentResult(ScrubCredentials(output))
}

// lookupCredentialedBinary checks if a command's binary has credential config.
// Returns the credential config and parsed args, or nil if not credentialed.
func (t *ExecTool) lookupCredentialedBinary(ctx context.Context, command string) (*store.SecureCLIBinary, string, []string) {
	if t.secureCLIStore == nil {
		return nil, "", nil
	}
	binary, args, err := parseCommandBinary(command)
	if err != nil {
		return nil, "", nil
	}
	// Get agent ID from context for scoped lookup
	agentID := store.AgentIDFromContext(ctx)
	var agentIDPtr *uuid.UUID
	if agentID != uuid.Nil {
		agentIDPtr = &agentID
	}
	cred, err := t.secureCLIStore.LookupByBinary(ctx, binary, agentIDPtr)
	if err != nil || cred == nil {
		return nil, "", nil
	}
	return cred, binary, args
}

// getenvDefault returns the value of an env var, or a default if not set.
func getenvDefault(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

// --- Structured error helpers ---

func credentialedShellOperatorError(command string, ops []string) *Result {
	return &Result{
		ForLLM: fmt.Sprintf("[CREDENTIALED EXEC] Shell operators not supported.\n"+
			"Detected: %s\n"+
			"This CLI runs in Direct Exec Mode — no shell operators (;  &&  ||  |  >  <  $()  ``).\n"+
			"Run the command without operators. Use --json or --format=json for structured output.",
			strings.Join(ops, ", ")),
		ForUser: "Command contains shell operators not supported in credentialed mode.",
		IsError: true,
	}
}

func credentialedPathError(binary string, err error) *Result {
	return &Result{
		ForLLM: fmt.Sprintf("[CREDENTIALED EXEC] Binary resolution failed.\n"+
			"Binary: %s\nError: %v\n"+
			"The binary may not be installed or the path doesn't match the configured path.",
			binary, err),
		ForUser: fmt.Sprintf("CLI binary %q not found or path mismatch.", binary),
		IsError: true,
	}
}

func credentialedDenyError(binary string, args []string, pattern string) *Result {
	return &Result{
		ForLLM: fmt.Sprintf("[CREDENTIALED EXEC] Command blocked by security policy.\n"+
			"Binary: %s\nArgs: %s\nMatched deny pattern: %s\n"+
			"This operation requires admin approval and cannot be performed automatically.",
			binary, strings.Join(args, " "), pattern),
		ForUser: fmt.Sprintf("Operation '%s %s' is blocked by security policy.", binary, strings.Join(args, " ")),
		IsError: true,
	}
}

func credentialedExecFailError(binary string, args []string, exitCode int, output string) *Result {
	return &Result{
		ForLLM: fmt.Sprintf("[CREDENTIALED EXEC] Command failed (exit code %d).\n"+
			"Binary: %s\nArgs: %s\n"+
			"Note: This runs in Direct Exec Mode — shell operators are NOT supported.\n"+
			"If you used shell operators, remove them and try again.\n\n%s",
			exitCode, binary, strings.Join(args, " "), output),
		ForUser: fmt.Sprintf("Command failed with exit code %d.", exitCode),
		IsError: true,
	}
}
