package proc

import (
	"bytes"
	"context"
	"os/exec"
	"time"
)

// ExecSpec 描述一次一次性（one-shot）命令执行的全部输入。
// 调用方（run.ExecService）负责前置鉴权、cwd / env 构造与命令白名单校验。
type ExecSpec struct {
	Command string
	Args    []string
	Dir     string
	Env     []string // 完整环境（宿主白名单 + 覆盖），nil 表示空环境而非继承宿主
	// Timeout 是硬上限：超时即对整组发起 TerminateGroup（树终止）并置 TimedOut。
	// <=0 时不设独立超时，仅受 ctx 约束。
	Timeout time.Duration
	// KillGrace 是树终止时的优雅宽限（Unix SIGTERM → grace → SIGKILL）。
	KillGrace time.Duration
	// MaxOutputBytes 是 stdout / stderr 各自的捕获上限；超出截断并置 truncated。
	// <=0 时使用 defaultMaxOutputBytes。
	MaxOutputBytes int
}

// ExecResult 是一次性执行的结构化结果。
type ExecResult struct {
	ExitCode   int
	Stdout     string
	Stderr     string
	Duration   time.Duration
	TimedOut   bool
	Truncated  bool // stdout 或 stderr 任一被截断时为 true
	StdoutSize int  // 截断前的实际字节数
	StderrSize int  // 截断前的实际字节数
}

const defaultMaxOutputBytes = 1 << 20 // 1 MiB

// RunOnce 同步执行 spec 描述的命令直至退出（或超时 / ctx 取消），捕获 stdout /
// stderr（各自封顶 MaxOutputBytes），返回退出码、时长、超时标志。
//
// 进程树管理沿用 RunSupervisor.Spawn 的次序：Prepare（Start 前）→ Start →
// Adopt（Start 后）→ 退出或超时时 TerminateGroup 整树清理。这样测试运行器派生的
// 子孙进程（go test 编译出的子进程、node 子进程等）在超时时也会被一并终止。
//
// 与长驻 profile 的差异：本函数阻塞到进程退出后才返回，不接入 relay；输出在内存
// 缓冲并随返回值一次性交回。错误仅在无法启动进程（exec.Start 失败）时返回；命令
// 以非零码退出不算 error，体现在 ExitCode 上。
func RunOnce(ctx context.Context, spec ExecSpec) (ExecResult, error) {
	maxOut := spec.MaxOutputBytes
	if maxOut <= 0 {
		maxOut = defaultMaxOutputBytes
	}

	cmd := exec.Command(spec.Command, spec.Args...)
	cmd.Dir = spec.Dir
	cmd.Env = spec.Env

	outBuf := &cappedBuffer{limit: maxOut}
	errBuf := &cappedBuffer{limit: maxOut}
	cmd.Stdout = outBuf
	cmd.Stderr = errBuf

	group := NewGroup()
	group.Prepare(cmd)

	started := time.Now()
	if err := cmd.Start(); err != nil {
		return ExecResult{}, err
	}
	if err := group.Adopt(cmd); err != nil {
		_ = cmd.Process.Kill()
		_ = cmd.Wait()
		group.Close()
		return ExecResult{}, err
	}

	// waitCh 携带 Wait() 的错误；done 在 Wait 返回后 close，供 TerminateGroup
	// 感知组长已被回收（语义对齐 RunSupervisor.monitorProcess 的 done channel）。
	waitCh := make(chan error, 1)
	done := make(chan struct{})
	go func() {
		err := cmd.Wait()
		waitCh <- err
		close(done)
	}()

	// 超时 / ctx-cancel watchdog：触发整树终止；result 由下方 select 收尾。
	var timeoutTimer <-chan time.Time
	if spec.Timeout > 0 {
		t := time.NewTimer(spec.Timeout)
		defer t.Stop()
		timeoutTimer = t.C
	}

	var timedOut bool
	var waitErr error
loop:
	for {
		select {
		case waitErr = <-waitCh:
			break loop
		case <-timeoutTimer:
			timedOut = true
			group.TerminateGroup(done, spec.KillGrace)
			waitErr = <-waitCh
			break loop
		case <-ctx.Done():
			group.TerminateGroup(done, spec.KillGrace)
			waitErr = <-waitCh
			break loop
		}
	}
	group.Close()

	res := ExecResult{
		ExitCode:   exitCodeOf(waitErr),
		Stdout:     outBuf.String(),
		Stderr:     errBuf.String(),
		Duration:   time.Since(started),
		TimedOut:   timedOut,
		Truncated:  outBuf.truncated || errBuf.truncated,
		StdoutSize: outBuf.total,
		StderrSize: errBuf.total,
	}
	return res, nil
}

// exitCodeOf 从 Cmd.Wait 的返回错误提取退出码。正常退出（nil）= 0；
// *exec.ExitError 取其 ExitCode（被信号杀死时为 -1）；其它错误统一 -1。
func exitCodeOf(waitErr error) int {
	if waitErr == nil {
		return 0
	}
	if ee, ok := waitErr.(*exec.ExitError); ok {
		return ee.ExitCode()
	}
	return -1
}

// cappedBuffer 是一个带上限的 io.Writer：累计写入 limit 字节后丢弃后续数据并置
// truncated，同时仍记录 total（截断前的总字节数）便于上层提示。
type cappedBuffer struct {
	buf       bytes.Buffer
	limit     int
	total     int
	truncated bool
}

func (c *cappedBuffer) Write(p []byte) (int, error) {
	c.total += len(p)
	if c.buf.Len() >= c.limit {
		c.truncated = true
		return len(p), nil // 谎报全写入：避免 cmd 因 short write 报错而中断
	}
	remain := c.limit - c.buf.Len()
	if len(p) > remain {
		c.buf.Write(p[:remain])
		c.truncated = true
		return len(p), nil
	}
	c.buf.Write(p)
	return len(p), nil
}

func (c *cappedBuffer) String() string { return c.buf.String() }
