package subprocess

import (
	"bufio"
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"os/exec"
)

const tailBytes = 64 * 1024

// Options describes one subprocess invocation.
type Options struct {
	Ctx     context.Context
	Command string    // shell snippet, run via /bin/sh -c
	Cwd     string    // working directory; "" means inherit
	Mirror  io.Writer // receives raw stdout+stderr line-by-line; may be nil
}

// Result is the outcome of Run. On success (exit 0): Err == nil. On non-zero
// exit: Err is *Error. On fork/exec/wait: Err is a wrapped error.
type Result struct {
	Output string
	Err    error
}

// Error indicates the subprocess ran and exited non-zero.
type Error struct {
	Code   int
	Output string
}

func (e *Error) Error() string {
	return fmt.Sprintf("subprocess exited with code %d", e.Code)
}

// Run starts the command, streams output to Mirror + the tail buffer, waits
// for completion, and returns the result.
func Run(opts Options) Result {
	cmd := exec.CommandContext(opts.Ctx, "/bin/sh", "-c", opts.Command)
	cmd.Dir = opts.Cwd
	stdoutPipe, pipeErr := cmd.StdoutPipe()
	if pipeErr != nil {
		return Result{Err: fmt.Errorf("subprocess stdout pipe: %w", pipeErr)}
	}
	cmd.Stderr = cmd.Stdout
	if startErr := cmd.Start(); startErr != nil {
		return Result{Err: fmt.Errorf("subprocess start: %w", startErr)}
	}

	tail := newTailBuffer(tailBytes)
	scanner := bufio.NewScanner(stdoutPipe)
	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
	for scanner.Scan() {
		line := scanner.Text() + "\n"
		if opts.Mirror != nil {
			_, _ = io.WriteString(opts.Mirror, line)
		}
		tail.Write([]byte(line))
	}
	if err := scanner.Err(); err != nil {
		slog.Error("subprocess scan", "cmd", opts.Command, "err", err)
	}
	output := tail.String()

	if waitErr := cmd.Wait(); waitErr != nil {
		var ee *exec.ExitError
		if errors.As(waitErr, &ee) {
			return Result{Output: output, Err: &Error{Code: ee.ExitCode(), Output: output}}
		}
		return Result{Output: output, Err: fmt.Errorf("subprocess wait: %w", waitErr)}
	}
	return Result{Output: output, Err: nil}
}

// tailBuffer keeps only the last `cap` bytes written to it.
type tailBuffer struct {
	buf  []byte
	head int // next write position; oldest is head when full
	full bool
}

func newTailBuffer(capBytes int) *tailBuffer {
	if capBytes <= 0 {
		capBytes = 1
	}
	return &tailBuffer{buf: make([]byte, capBytes)}
}

func (t *tailBuffer) Write(p []byte) (int, error) {
	n := len(p)
	if n == 0 {
		return 0, nil
	}
	if n >= len(t.buf) {
		copy(t.buf, p[n-len(t.buf):])
		t.head = 0
		t.full = true
		return n, nil
	}
	written := copy(t.buf[t.head:], p)
	if written < n {
		copy(t.buf, p[written:])
	}
	t.head = (t.head + n) % len(t.buf)
	if t.head == 0 {
		t.full = true
	}
	return n, nil
}

func (t *tailBuffer) String() string {
	if !t.full {
		return string(t.buf[:t.head])
	}
	return string(t.buf[t.head:]) + string(t.buf[:t.head])
}
