package subprocess

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"os/exec"
)

// ExecOptions describes one direct exec invocation (no shell). Use this form
// when the caller has Bin + Args as separate values (vmctl, kubectl, aws CLI);
// use the shell-form Options.Run for rendered shell snippets.
type ExecOptions struct {
	Ctx    context.Context
	Bin    string
	Args   []string
	Cwd    string    // optional; "" inherits the caller's working directory
	Env    []string  // optional; nil inherits the caller's environment
	Stdout io.Writer // optional mirror for stdout; nil discards
	Stderr io.Writer // optional mirror for stderr; nil discards
}

// ExecResult is the outcome of Exec. Stdout and stderr are captured
// separately so callers can include stderr verbatim in error messages and parse
// stdout as structured data (e.g. JSON).
type ExecResult struct {
	Stdout string // captured stdout tail (last 64 KiB)
	Stderr string // captured stderr tail (last 64 KiB)
	Err    error  // *Error on non-zero exit; wrapped error on fork/exec/wait
}

// Exec starts Bin with Args, mirrors stdout/stderr to the optional writers,
// and returns the captured tails + error. When the process exits non-zero, Err
// is *Error (with Output = stderr tail). When fork/exec/wait fails,
// Err wraps the underlying error.
//
// Unlike Run (single shared tail for stdout+stderr via shell 2>&1), Exec keeps
// the two streams separate.
func Exec(opts ExecOptions) ExecResult {
	cmd := exec.CommandContext(opts.Ctx, opts.Bin, opts.Args...)
	cmd.Dir = opts.Cwd
	cmd.Env = opts.Env
	var stdoutTail, stderrTail bytes.Buffer
	tailStdout := newTailBuffer(tailBytes)
	tailStderr := newTailBuffer(tailBytes)
	cmd.Stdout = io.MultiWriter(discardNil(opts.Stdout), newByteWriter(tailStdout))
	cmd.Stderr = io.MultiWriter(discardNil(opts.Stderr), newByteWriter(tailStderr))
	err := cmd.Run()
	stdoutTail.WriteString(tailStdout.String())
	stderrTail.WriteString(tailStderr.String())
	if err == nil {
		return ExecResult{Stdout: stdoutTail.String(), Stderr: stderrTail.String()}
	}
	var ee *exec.ExitError
	if errors.As(err, &ee) {
		return ExecResult{
			Stdout: stdoutTail.String(),
			Stderr: stderrTail.String(),
			Err:    &Error{Code: ee.ExitCode(), Output: stderrTail.String()},
		}
	}
	return ExecResult{
		Stdout: stdoutTail.String(),
		Stderr: stderrTail.String(),
		Err:    fmt.Errorf("exec %s: %w", opts.Bin, err),
	}
}

// discardNil returns w if non-nil, else io.Discard.
func discardNil(w io.Writer) io.Writer {
	if w == nil {
		return io.Discard
	}
	return w
}

func newByteWriter(t *tailBuffer) io.Writer { return t }
