package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os/exec"
	"strings"
	"time"
)

// CapabilityRequest envelope minimo POC (sin firma todavia — issue 0134 v0.2).
type CapabilityRequest struct {
	RequestID  string   `json:"request_id"`
	Capability string          `json:"capability"`
	Args       json.RawMessage `json:"args"`
	Nonce      string          `json:"nonce"`
	Timestamp  int64           `json:"ts"`
}

// parseArgsArray intenta decode args como []string. Si args es un objeto
// con campo "argv" (formato MCP), extrae argv. Si es array plano, lo devuelve.
func parseArgsArray(raw json.RawMessage) ([]string, error) {
	if len(raw) == 0 {
		return nil, nil
	}
	// Intento 1: array
	var arr []string
	if err := json.Unmarshal(raw, &arr); err == nil {
		return arr, nil
	}
	// Intento 2: object con argv
	var obj map[string]any
	if err := json.Unmarshal(raw, &obj); err != nil {
		return nil, fmt.Errorf("args must be array or object with argv: %w", err)
	}
	if v, ok := obj["argv"]; ok {
		switch t := v.(type) {
		case []any:
			out := make([]string, len(t))
			for i, x := range t {
				out[i] = fmt.Sprintf("%v", x)
			}
			return out, nil
		case []string:
			return t, nil
		}
	}
	return nil, fmt.Errorf("args object missing argv array")
}

// parseArgsMap decodifica args como map[string]any para capabilities que
// reciben objeto. Acepta tambien array (lo wrap como {argv:[...]}).
func parseArgsMap(raw json.RawMessage) (map[string]any, error) {
	if len(raw) == 0 {
		return nil, nil
	}
	var obj map[string]any
	if err := json.Unmarshal(raw, &obj); err == nil {
		return obj, nil
	}
	// Fallback: array → {argv: [...]}
	var arr []any
	if err := json.Unmarshal(raw, &arr); err != nil {
		return nil, fmt.Errorf("args must be object or array: %w", err)
	}
	return map[string]any{"argv": arr}, nil
}

type CapabilityResponse struct {
	RequestID  string `json:"request_id"`
	OK         bool   `json:"ok"`
	Result     any    `json:"result,omitempty"`
	Error      string `json:"error,omitempty"`
	DurationMs int64  `json:"duration_ms"`
	AuditHash  string `json:"audit_hash,omitempty"`
}

func capabilityHandler(mf *Manifest, audit *Audit) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "POST only")
			return
		}
		var req CapabilityRequest
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			writeError(w, http.StatusBadRequest, "bad_request", err.Error())
			return
		}
		if req.RequestID == "" || req.Capability == "" {
			writeError(w, http.StatusBadRequest, "bad_request", "request_id and capability required")
			return
		}

		cap := mf.CapabilityByName(req.Capability)
		if cap == nil {
			writeError(w, http.StatusForbidden, "capability_denied",
				fmt.Sprintf("capability %q not in manifest", req.Capability))
			return
		}

		start := time.Now()
		var result any
		var execErr error
		exitCode := 0
		var shellEvalExtra *ShellEvalRecord

		switch req.Capability {
		case "shell.exec":
			argv, perr := parseArgsArray(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runShellExec(cap, argv)
		case "shell.eval":
			rawMap, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			var r map[string]any
			r, exitCode, shellEvalExtra, execErr = runShellEval(cap, rawMap)
			result = r
		case "fs.read":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runFsRead(cap, m)
		case "fs.write":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runFsWrite(cap, m)
		case "fs.list":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runFsList(cap, m)
		case "fs.stat":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runFsStat(cap, m)
		case "git.clone":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runGitClone(cap, m)
		case "git.commit":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runGitCommit(cap, m)
		case "git.push":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runGitPush(cap, m)
		case "pkg.search":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runPkgSearch(cap, m)
		case "proc.list":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runProcList(cap, m)
		case "docker.container.list":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runDockerList(cap, m)
		case "docker.container.exec":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runDockerExec(cap, m)
		case "docker.container.logs":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			result, exitCode, execErr = runDockerLogs(cap, m)
		case "browser.list_tabs", "browser.navigate", "browser.click_text",
			"browser.evaluate", "browser.screenshot", "browser.type_text",
			"browser.get_html", "browser.launch_chrome":
			m, perr := parseArgsMap(req.Args)
			if perr != nil {
				execErr = perr
				break
			}
			op := strings.TrimPrefix(req.Capability, "browser.")
			result, exitCode, execErr = runBrowserOp(cap, op, m)
		default:
			execErr = fmt.Errorf("capability %q not implemented yet in POC", req.Capability)
		}

		dur := time.Since(start).Milliseconds()

		var hash string
		var herr error
		if req.Capability == "shell.eval" && shellEvalExtra != nil {
			hash, herr = audit.AppendVerbose(req.RequestID, req.Capability, req.Args, exitCode, *shellEvalExtra)
		} else {
			hash, herr = audit.Append(req.RequestID, req.Capability, req.Args, exitCode)
		}
		if herr != nil {
			// audit es critico — si falla, devolvemos error pero no perdemos el output.
			writeJSON(w, http.StatusInternalServerError, CapabilityResponse{
				RequestID:  req.RequestID,
				OK:         false,
				Error:      "audit_failed: " + herr.Error(),
				DurationMs: dur,
			})
			return
		}

		resp := CapabilityResponse{
			RequestID:  req.RequestID,
			DurationMs: dur,
			AuditHash:  hash,
		}
		if execErr != nil {
			resp.OK = false
			resp.Error = execErr.Error()
			writeJSON(w, http.StatusInternalServerError, resp)
			return
		}
		resp.OK = true
		resp.Result = result
		writeJSON(w, http.StatusOK, resp)
	}
}

// parseShellEvalArgs decodifica los args de la envelope shell.eval.
// Convencion: req.Args puede ser:
//   - len(args) == 1 y args[0] es un JSON object {"cmd":..., "shell":..., "cwd":...}
//   - len(args) >= 1 y args[0] = cmd, args[1] (opcional) = cwd, args[2] (opcional) = shell
//
// Devuelve un map[string]any compatible con runShellEval.
func parseShellEvalArgs(args []string) (map[string]any, error) {
	if len(args) == 0 {
		return nil, fmt.Errorf("shell.eval requires at least 1 arg")
	}
	// Intento 1: JSON object string.
	trim := strings.TrimSpace(args[0])
	if strings.HasPrefix(trim, "{") && strings.HasSuffix(trim, "}") {
		var m map[string]any
		if err := json.Unmarshal([]byte(trim), &m); err == nil {
			return m, nil
		}
		// caemos al fallback
	}
	out := map[string]any{
		"cmd": args[0],
	}
	if len(args) >= 2 && args[1] != "" {
		out["cwd"] = args[1]
	}
	if len(args) >= 3 && args[2] != "" {
		out["shell"] = args[2]
	}
	return out, nil
}

// runShellExec implementa shell.exec con whitelist del manifest local.
// POC INLINE: el flow 0009 v0.2 lo migra a shell_exec_whitelist_go_infra del registry.
func runShellExec(cap *Capability, args []string) (any, int, error) {
	if len(args) == 0 {
		return nil, -1, fmt.Errorf("no argv provided")
	}
	if len(cap.BinariesAllowed) == 0 {
		return nil, -1, fmt.Errorf("no binaries whitelisted for shell.exec")
	}
	bin := args[0]
	allowed := false
	for _, b := range cap.BinariesAllowed {
		if b == bin || strings.HasSuffix(bin, "/"+b) {
			allowed = true
			break
		}
	}
	if !allowed {
		return nil, -1, fmt.Errorf("binary %q not in whitelist %v", bin, cap.BinariesAllowed)
	}

	cmd := exec.Command(args[0], args[1:]...) // #nosec G204 — whitelist enforced above
	out, err := cmd.CombinedOutput()
	exitCode := 0
	if err != nil {
		if ee, ok := err.(*exec.ExitError); ok {
			exitCode = ee.ExitCode()
		} else {
			return nil, -1, err
		}
	}
	return map[string]any{
		"stdout":    string(out),
		"exit_code": exitCode,
	}, exitCode, nil
}
