package byoc

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strings"

	"github.com/tidbcloud/controlplane9/internal/verbose"
)

type Command struct {
	Name  string
	Args  []string
	Dir   string
	Env   []string
	Stdin []byte
}

func (c Command) String() string {
	return strings.TrimSpace(c.Name + " " + strings.Join(c.Args, " "))
}

type Runner interface {
	Run(ctx context.Context, command Command) ([]byte, error)
}

type ExecRunner struct{}

// Run keeps Command immutable across the execution boundary.
//
//nolint:gocritic // Passing the command by value prevents runners from mutating a recorded request.
func (ExecRunner) Run(ctx context.Context, command Command) ([]byte, error) {
	if strings.TrimSpace(command.Name) == "" {
		return nil, fmt.Errorf("BYOC command name is required")
	}
	//nolint:gosec // Executables and arguments are selected by the reviewed BYOC driver.
	cmd := exec.CommandContext(ctx, command.Name, command.Args...)
	cmd.Dir = command.Dir
	cmd.Env = append(os.Environ(), command.Env...)
	if command.Stdin != nil {
		cmd.Stdin = bytes.NewReader(command.Stdin)
	}
	output, err := verbose.RunCommand(cmd)
	if err != nil {
		return output, &CommandError{Command: command.String(), Output: output, Cause: err}
	}
	return output, nil
}

type CommandError struct {
	Command string
	Output  []byte
	Cause   error
}

func (e *CommandError) Error() string {
	if e == nil {
		return "BYOC command failed"
	}
	return fmt.Sprintf("%s failed: %v", e.Command, e.Cause)
}

func (e *CommandError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Cause
}

func commandFailureText(output []byte, err error) string {
	if len(output) > 0 {
		return string(output)
	}
	var commandErr *CommandError
	if errors.As(err, &commandErr) {
		return string(commandErr.Output)
	}
	if err == nil {
		return ""
	}
	return err.Error()
}
