package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
	"time"
)

// ShellEvalArgs es el payload args para shell.eval (decodificado desde un map).
type ShellEvalArgs struct {
	Cmd   string `json:"cmd"`
	Shell string `json:"shell,omitempty"` // bash|powershell|auto (default auto)
	CWD   string `json:"cwd,omitempty"`
}

// hardBlocklist son patrones bloqueados SIEMPRE (no configurable).
// Match case-insensitive. Cualquier match = rechazo, no se puede aprobar.
var hardBlocklist = []string{
	`rm\s+-rf\s+/`,
	`rm\s+-rf\s+\$HOME`,
	`rm\s+-rf\s+~/?\s*$`,
	`dd\s+if=`,
	`mkfs\.`,
	`chmod\s+-R\s+777\s+/`,
	`curl[^|]*\|\s*sh`,
	`wget[^|]*\|\s*sh`,
	`wget[^|]*\|\s*bash`,
	`>\s*/dev/sd[a-z]`,
	`>\s*/dev/nvme`,
	`:(){\s*:\|:&\s*};:`,
	`:\(\)\s*{\s*:\|:&\s*};:`,
	`shutdown\s+-h`,
	`reboot\b`,
	`halt\b`,
	`init\s+0`,
	`init\s+6`,
	`format\s+[c-z]:`,
}

// defaultAutoApprove se usa cuando Capability.AutoApprove esta vacio.
var defaultAutoApprove = []string{
	`^git\s`,
	`^ls\s`,
	`^cat\s`,
	`^grep\s`,
	`^find\s`,
	`^docker\s+ps`,
	`^docker\s+logs`,
	`^ps\b`,
	`^df\s`,
	`^du\s`,
	`^echo\s`,
	`^pwd$`,
	`^hostname$`,
	`^whoami$`,
	`^date$`,
	`^uname`,
}

// approvalQueuePath path del jsonl que persiste solicitudes pendientes
// de aprobacion (mecanismo placeholder hasta 0144f).
var approvalQueuePath = filepath.Join("local_files", "approval_queue.jsonl")

// runShellEval implementa la capability shell.eval.
//
// Devuelve (result, exitCode, err). Si err != nil, result deberia ser nil.
// extra contiene los campos cleartext (cmd/cwd/shell/stdout/stderr) que
// el caller pasara a audit.AppendVerbose. Si no se ejecuto (blocklist /
// approval_required), extra puede ser nil.
func runShellEval(cap *Capability, raw map[string]any) (result map[string]any, exitCode int, extra *ShellEvalRecord, err error) {
	// 1. Parse args desde raw via json roundtrip.
	rawJSON, jerr := json.Marshal(raw)
	if jerr != nil {
		return nil, -1, nil, fmt.Errorf("marshal raw args: %w", jerr)
	}
	var args ShellEvalArgs
	if jerr := json.Unmarshal(rawJSON, &args); jerr != nil {
		return nil, -1, nil, fmt.Errorf("unmarshal args: %w", jerr)
	}
	args.Cmd = strings.TrimSpace(args.Cmd)
	if args.Cmd == "" {
		return nil, -1, nil, fmt.Errorf("cmd is required")
	}

	// 2. Hardcoded blocklist + capability extension.
	blocklist := append([]string{}, hardBlocklist...)
	blocklist = append(blocklist, cap.Blocklist...)
	for _, pat := range blocklist {
		re, rerr := regexp.Compile(`(?i)` + pat)
		if rerr != nil {
			// pattern operator invalido -> skip pero no bloqueamos.
			continue
		}
		if re.MatchString(args.Cmd) {
			return nil, -1, nil, fmt.Errorf("blocked: matches hardcoded safety blocklist (%s)", pat)
		}
	}

	// 3. OS detect + shell selection.
	chosenShell, sErr := resolveShell(cap, args.Shell)
	if sErr != nil {
		return nil, -1, nil, sErr
	}

	// 4. Auto-approval check.
	autoApprove := cap.AutoApprove
	if len(autoApprove) == 0 {
		autoApprove = defaultAutoApprove
	}
	approved := false
	for _, pat := range autoApprove {
		re, rerr := regexp.Compile(pat)
		if rerr != nil {
			continue
		}
		if re.MatchString(args.Cmd) {
			approved = true
			break
		}
	}

	approvalStatus := "none-required"
	if approved {
		approvalStatus = "auto-approved"
	} else if cap.RequiresApproval {
		// 5. requires_approval true + not auto -> queue + error
		if err := enqueueApproval(args, cap); err != nil {
			return nil, -1, nil, fmt.Errorf("enqueue approval failed: %w", err)
		}
		return nil, -1, nil, fmt.Errorf("approval_required: cmd queued at %s", approvalQueuePath)
	}

	// 6. Exec.
	timeoutS := cap.TimeoutSeconds
	if timeoutS <= 0 {
		timeoutS = 60
	}
	maxOut := cap.MaxOutputBytes
	if maxOut <= 0 {
		maxOut = 1024 * 1024 // 1MB
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutS)*time.Second)
	defer cancel()

	var c *exec.Cmd
	switch chosenShell {
	case "bash":
		c = exec.CommandContext(ctx, "bash", "-c", args.Cmd)
	case "powershell":
		bin := "powershell.exe"
		if _, lerr := exec.LookPath(bin); lerr != nil {
			if _, lerr2 := exec.LookPath("pwsh"); lerr2 == nil {
				bin = "pwsh"
			}
		}
		c = exec.CommandContext(ctx, bin, "-NoProfile", "-NonInteractive", "-Command", args.Cmd)
	default:
		return nil, -1, nil, fmt.Errorf("unknown shell %q", chosenShell)
	}

	if args.CWD != "" {
		if !filepath.IsAbs(args.CWD) {
			return nil, -1, nil, fmt.Errorf("cwd must be absolute path")
		}
		st, serr := os.Stat(args.CWD)
		if serr != nil || !st.IsDir() {
			return nil, -1, nil, fmt.Errorf("cwd does not exist or not a dir: %s", args.CWD)
		}
		c.Dir = args.CWD
	}

	var stdoutBuf, stderrBuf strings.Builder
	c.Stdout = &capWriter{buf: &stdoutBuf, max: maxOut}
	c.Stderr = &capWriter{buf: &stderrBuf, max: maxOut}

	start := time.Now()
	runErr := c.Run()
	dur := time.Since(start).Milliseconds()

	exitCode = 0
	if runErr != nil {
		if ee, ok := runErr.(*exec.ExitError); ok {
			exitCode = ee.ExitCode()
		} else if ctx.Err() == context.DeadlineExceeded {
			exitCode = 124 // convencion timeout
		} else {
			exitCode = -1
		}
	}

	stdoutS := stdoutBuf.String()
	stderrS := stderrBuf.String()
	truncated := false
	if sw, ok := c.Stdout.(*capWriter); ok && sw.truncated {
		truncated = true
	}
	if sw, ok := c.Stderr.(*capWriter); ok && sw.truncated {
		truncated = true
	}

	extra = &ShellEvalRecord{
		Cmd:    args.Cmd,
		CWD:    args.CWD,
		Shell:  chosenShell,
		Stdout: stdoutS,
		Stderr: stderrS,
	}

	result = map[string]any{
		"stdout":          stdoutS,
		"stderr":          stderrS,
		"exit_code":       exitCode,
		"shell":           chosenShell,
		"approval_status": approvalStatus,
		"truncated":       truncated,
		"duration_ms":     dur,
		"cmd_executed":    args.Cmd,
	}
	return result, exitCode, extra, nil
}

// resolveShell decide bash o powershell segun GOOS + override (cap.ShellMode/args.Shell).
// Reglas:
//   - args.Shell (call-level) > cap.ShellMode (manifest) > GOOS-default.
//   - "auto" = GOOS-default.
//   - powershell en linux solo si pwsh esta en PATH.
//   - bash en windows: rechazado.
func resolveShell(cap *Capability, callShell string) (string, error) {
	mode := strings.ToLower(strings.TrimSpace(callShell))
	if mode == "" {
		mode = strings.ToLower(strings.TrimSpace(cap.ShellMode))
	}
	if mode == "" || mode == "auto" {
		switch runtime.GOOS {
		case "windows":
			return "powershell", nil
		default:
			return "bash", nil
		}
	}
	switch mode {
	case "bash":
		if runtime.GOOS == "windows" {
			return "", fmt.Errorf("bash on windows not supported by this agent")
		}
		return "bash", nil
	case "powershell":
		if runtime.GOOS != "windows" {
			if _, err := exec.LookPath("pwsh"); err != nil {
				return "", fmt.Errorf("powershell requested but pwsh not in PATH on %s", runtime.GOOS)
			}
		}
		return "powershell", nil
	default:
		return "", fmt.Errorf("invalid shell mode %q (want bash|powershell|auto)", mode)
	}
}

// enqueueApproval anade una entrada al jsonl. POC: el agent LLM observa el fichero.
// En 0144f pasa a Matrix reactions.
func enqueueApproval(args ShellEvalArgs, cap *Capability) error {
	if err := os.MkdirAll(filepath.Dir(approvalQueuePath), 0700); err != nil {
		return err
	}
	entry := map[string]any{
		"ts":         time.Now().Unix(),
		"request_id": fmt.Sprintf("approval-%d", time.Now().UnixNano()),
		"cmd":        args.Cmd,
		"cwd":        args.CWD,
		"capability": cap.Name,
		"status":     "pending",
	}
	b, err := json.Marshal(entry)
	if err != nil {
		return err
	}
	f, err := os.OpenFile(approvalQueuePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
	if err != nil {
		return err
	}
	defer f.Close()
	if _, err := f.Write(append(b, '\n')); err != nil {
		return err
	}
	return nil
}

// capWriter es un io.Writer que trunca a max bytes y flag truncated.
type capWriter struct {
	buf       *strings.Builder
	max       int
	truncated bool
}

func (w *capWriter) Write(p []byte) (int, error) {
	remaining := w.max - w.buf.Len()
	if remaining <= 0 {
		w.truncated = true
		return len(p), nil // descartamos pero pretendemos exito para no romper proceso
	}
	if len(p) > remaining {
		w.buf.Write(p[:remaining])
		w.truncated = true
		return len(p), nil
	}
	w.buf.Write(p)
	return len(p), nil
}
