// Copyright Earl Warren <contact@earl-warren.org>
// Copyright Loïc Dachary <loic@dachary.org>
// SPDX-License-Identifier: MIT

package util

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

	"code.forgejo.org/f3/gof3/v3/logger"
)

type CommandOptions struct {
	ExitCodes []int
	Log       logger.MessageInterface
}

func (o *CommandOptions) setDefaults() {
	if o.ExitCodes == nil {
		o.ExitCodes = []int{0}
	}
	if o.Log == nil {
		o.Log = logger.NewLogger()
	}
}

func CommandWithErr(ctx context.Context, options CommandOptions, prog string, args ...string) (err error) {
	defer func() {
		if r := recover(); r != nil {
			var ok bool
			err, ok = r.(error)
			if !ok {
				panic(r)
			}
		}
	}()

	CommandWithOptions(ctx, options, prog, args...)

	return nil
}

func Command(ctx context.Context, log logger.MessageInterface, prog string, args ...string) string {
	options := CommandOptions{
		Log: log,
	}
	return CommandWithOptions(ctx, options, prog, args...)
}

func CommandWithOptions(ctx context.Context, options CommandOptions, prog string, args ...string) string {
	if ctx == nil {
		panic("ctx context.Context is nil")
	}
	options.setDefaults()
	options.Log.Log(1, logger.Trace, "%s\n", args)
	cmd := exec.Command(prog, args...)
	SetSysProcAttribute(cmd)
	var out bytes.Buffer
	cmd.Stdout = &out
	cmd.Stderr = &out
	cmd.Env = append(
		cmd.Env,
		"GIT_TERMINAL_PROMPT=0",
	)

	err := cmd.Start()
	if err != nil {
		panic(err)
	}
	ctxErr := watchCtx(ctx, cmd.Process)
	err = cmd.Wait()
	interruptErr := <-ctxErr
	// If cmd.Wait returned an error, prefer that.
	// Otherwise, report any error from the interrupt goroutine.
	if interruptErr != nil && err == nil {
		err = interruptErr
	}

	var code int
	if err == nil {
		code = 0
	} else if exiterr, ok := err.(*exec.ExitError); ok {
		code = exiterr.ExitCode()
		if code == -1 {
			panic("killed")
		}
	} else {
		panic(err)
	}

	found := false
	for _, valid := range options.ExitCodes {
		if valid == code {
			found = true
			break
		}
	}
	if !found {
		panic(fmt.Errorf("%w: exit code: %d, %v, %v, %v", err, code, out.String(), prog, args))
	}
	options.Log.Log(1, logger.Trace, "%s\n", out.String())
	return out.String()
}

// wrappedError wraps an error without relying on fmt.Errorf.
type wrappedError struct {
	prefix string
	err    error
}

func (w wrappedError) Error() string {
	return w.prefix + ": " + w.err.Error()
}

func (w wrappedError) Unwrap() error {
	return w.err
}

func watchCtx(ctx context.Context, p *os.Process) <-chan error {
	if ctx == nil {
		return nil
	}

	errc := make(chan error)
	go func() {
		select {
		case errc <- nil:
			return
		case <-ctx.Done():
		}

		var err error
		if killErr := kill(p); killErr == nil {
			// We appear to have successfully delivered a kill signal, so any
			// program behavior from this point may be due to ctx.
			err = ctx.Err()
		} else if !errors.Is(killErr, os.ErrProcessDone) {
			err = wrappedError{
				prefix: "util: exec: error sending signal",
				err:    killErr,
			}
		}
		errc <- err
	}()

	return errc
}
