package track4

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

type runScriptRequest struct {
	Script string   `json:"script"`
	Args   []string `json:"args"`
}

const maxOperatorScriptOutputBytes = 64 << 10

type cappedBuffer struct {
	buf       bytes.Buffer
	maxBytes  int
	truncated bool
}

func (c *cappedBuffer) Write(p []byte) (int, error) {
	if len(p) == 0 {
		return 0, nil
	}

	remaining := c.maxBytes - c.buf.Len()
	if remaining > 0 {
		if len(p) > remaining {
			_, _ = c.buf.Write(p[:remaining])
			c.truncated = true
			return len(p), nil
		}
		_, _ = c.buf.Write(p)
		return len(p), nil
	}

	c.truncated = true
	return len(p), nil
}

func (c *cappedBuffer) String() string {
	if !c.truncated {
		return c.buf.String()
	}
	return fmt.Sprintf("%s\n[truncated after %d bytes]", c.buf.String(), c.maxBytes)
}

func (c *cappedBuffer) Len() int {
	return c.buf.Len()
}

// HandleRunScript handles POST /api/v1/track4/operator/run-script
// Requires Track 4 auth, IP whitelist, OPERATOR_SCRIPTS_ROOT, and OPERATOR_SCRIPT_ALLOWLIST.
func (s *Server) HandleRunScript(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed")
		return
	}

	operatorAddr, _ := r.Context().Value("user_address").(string)
	if operatorAddr == "" {
		writeError(w, http.StatusUnauthorized, "unauthorized", "Operator address required")
		return
	}
	ipAddr := clientIPAddress(r)
	if whitelisted, _ := s.roleMgr.IsIPWhitelisted(r.Context(), operatorAddr, ipAddr); !whitelisted {
		writeError(w, http.StatusForbidden, "forbidden", "IP address not whitelisted")
		return
	}

	root := strings.TrimSpace(os.Getenv("OPERATOR_SCRIPTS_ROOT"))
	if root == "" {
		writeError(w, http.StatusServiceUnavailable, "service_unavailable", "OPERATOR_SCRIPTS_ROOT not configured")
		return
	}
	rootAbs, err := filepath.Abs(root)
	if err != nil || rootAbs == "" {
		writeError(w, http.StatusInternalServerError, "internal_error", "invalid OPERATOR_SCRIPTS_ROOT")
		return
	}

	allowRaw := strings.TrimSpace(os.Getenv("OPERATOR_SCRIPT_ALLOWLIST"))
	if allowRaw == "" {
		writeError(w, http.StatusServiceUnavailable, "service_unavailable", "OPERATOR_SCRIPT_ALLOWLIST not configured")
		return
	}
	var allow []string
	for _, p := range strings.Split(allowRaw, ",") {
		p = strings.TrimSpace(p)
		if p != "" {
			allow = append(allow, p)
		}
	}
	if len(allow) == 0 {
		writeError(w, http.StatusServiceUnavailable, "service_unavailable", "OPERATOR_SCRIPT_ALLOWLIST empty")
		return
	}

	var reqBody runScriptRequest
	dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
	dec.DisallowUnknownFields()
	if err := dec.Decode(&reqBody); err != nil {
		writeError(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
		return
	}
	script := strings.TrimSpace(reqBody.Script)
	if script == "" || strings.Contains(script, "..") {
		writeError(w, http.StatusBadRequest, "bad_request", "invalid script path")
		return
	}
	if len(reqBody.Args) > 24 {
		writeError(w, http.StatusBadRequest, "bad_request", "too many args (max 24)")
		return
	}
	for _, a := range reqBody.Args {
		if strings.Contains(a, "\x00") {
			writeError(w, http.StatusBadRequest, "bad_request", "invalid arg")
			return
		}
	}

	candidate := filepath.Join(rootAbs, filepath.Clean(script))
	if rel, err := filepath.Rel(rootAbs, candidate); err != nil || strings.HasPrefix(rel, "..") {
		writeError(w, http.StatusForbidden, "forbidden", "script outside OPERATOR_SCRIPTS_ROOT")
		return
	}

	relPath, _ := filepath.Rel(rootAbs, candidate)
	relPath = filepath.Clean(filepath.ToSlash(relPath))
	allowed := false
	for _, a := range allow {
		normalizedAllow := filepath.Clean(filepath.ToSlash(a))
		if normalizedAllow == relPath {
			allowed = true
			break
		}
	}
	if !allowed {
		writeError(w, http.StatusForbidden, "forbidden", "script not in OPERATOR_SCRIPT_ALLOWLIST")
		return
	}

	st, err := os.Stat(candidate)
	if err != nil || st.IsDir() {
		writeError(w, http.StatusNotFound, "not_found", "script not found")
		return
	}
	isShell := strings.HasSuffix(strings.ToLower(candidate), ".sh")
	if !isShell && st.Mode()&0o111 == 0 {
		writeError(w, http.StatusForbidden, "forbidden", "refusing to run non-executable file (use .sh or chmod +x)")
		return
	}

	timeout := 120 * time.Second
	if v := strings.TrimSpace(os.Getenv("OPERATOR_SCRIPT_TIMEOUT_SEC")); v != "" {
		if sec, err := parsePositiveInt(v); err == nil && sec > 0 && sec < 600 {
			timeout = time.Duration(sec) * time.Second
		}
	}

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

	s.roleMgr.LogOperatorEvent(r.Context(), "operator_script_run", &s.chainID, operatorAddr, "operator/run-script", "execute",
		map[string]interface{}{
			"script": relPath,
			"argc":   len(reqBody.Args),
		}, ipAddr, r.UserAgent())

	var cmd *exec.Cmd
	if isShell {
		args := append([]string{candidate}, reqBody.Args...)
		cmd = exec.CommandContext(ctx, "/bin/bash", args...)
	} else {
		cmd = exec.CommandContext(ctx, candidate, reqBody.Args...)
	}
	var stdout, stderr cappedBuffer
	stdout.maxBytes = maxOperatorScriptOutputBytes
	stderr.maxBytes = maxOperatorScriptOutputBytes
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	runErr := cmd.Run()

	exit := 0
	timedOut := errors.Is(ctx.Err(), context.DeadlineExceeded)
	if runErr != nil {
		var ee *exec.ExitError
		if errors.As(runErr, &ee) {
			exit = ee.ExitCode()
		} else if timedOut {
			exit = -1
		} else {
			writeError(w, http.StatusInternalServerError, "internal_error", runErr.Error())
			return
		}
	}

	status := "ok"
	if timedOut {
		status = "timed_out"
	} else if exit != 0 {
		status = "nonzero_exit"
	}
	s.roleMgr.LogOperatorEvent(r.Context(), "operator_script_result", &s.chainID, operatorAddr, "operator/run-script", status,
		map[string]interface{}{
			"script":       relPath,
			"argc":         len(reqBody.Args),
			"exit_code":    exit,
			"timed_out":    timedOut,
			"stdout_bytes": stdout.Len(),
			"stderr_bytes": stderr.Len(),
			"stdout_truncated": stdout.truncated,
			"stderr_truncated": stderr.truncated,
		}, ipAddr, r.UserAgent())

	resp := map[string]interface{}{
		"data": map[string]interface{}{
			"script":            relPath,
			"exit_code":         exit,
			"stdout":            strings.TrimSpace(stdout.String()),
			"stderr":            strings.TrimSpace(stderr.String()),
			"timed_out":         timedOut,
			"stdout_truncated":  stdout.truncated,
			"stderr_truncated":  stderr.truncated,
		},
	}
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(resp)
}

func parsePositiveInt(s string) (int, error) {
	var n int
	for _, c := range s {
		if c < '0' || c > '9' {
			return 0, errors.New("not digits")
		}
		n = n*10 + int(c-'0')
		if n > 1e6 {
			return 0, errors.New("too large")
		}
	}
	if n == 0 {
		return 0, errors.New("zero")
	}
	return n, nil
}
