package errors

import (
	"errors"
	"fmt"

	goerrors "github.com/go-errors/errors"
	"github.com/urfave/cli/v2"
)

// Errorf creates a new error and wraps in an Error type that contains the stack trace.
func Errorf(message string, args ...interface{}) error {
	err := fmt.Errorf(message, args...)
	return goerrors.Wrap(err, 1)
}

// If this error is returned, the program should exit with the given exit code.
type ErrorWithExitCode struct {
	Err      error
	ExitCode int
}

func (err ErrorWithExitCode) Error() string {
	return err.Err.Error()
}

// Wrap the given error in an Error type that contains the stack trace. If the given error already has a stack trace,
// it is used directly. If the given error is nil, return nil.
func WithStackTrace(err error) error {
	if err == nil {
		return nil
	}

	return goerrors.Wrap(err, 1)
}

// Wrap the given error in an Error type that contains the stack trace and has the given message prepended as part of
// the error message. If the given error already has a stack trace, it is used directly. If the given error is nil,
// return nil.
func WithStackTraceAndPrefix(err error, message string, args ...interface{}) error {
	if err == nil {
		return nil
	}

	return goerrors.WrapPrefix(err, fmt.Sprintf(message, args...), 1)
}

// Returns true if actual is the same type of error as expected. This method unwraps the given error objects (if they
// are wrapped in objects with a stacktrace) and then does a simple equality check on them.
func IsError(actual error, expected error) bool {
	return goerrors.Is(actual, expected)
}

// If the given error is a wrapper that contains a stacktrace, unwrap it and return the original, underlying error.
// In all other cases, return the error unchanged
func Unwrap(err error) error {
	if err == nil {
		return nil
	}

	goError, isGoError := err.(*goerrors.Error)
	if isGoError {
		return goError.Err
	}

	return err
}

// PrintErrorWithStackTrace converts the given error to a string, including the deepest stack trace if available.
func PrintErrorWithStackTrace(err error) string {
	if err == nil {
		return ""
	}

	goerr := &goerrors.Error{Err: err}

	for {
		if err, ok := err.(*goerrors.Error); ok {
			goerr = err
		}

		if err = errors.Unwrap(err); err == nil {
			break
		}
	}

	return goerr.ErrorStack()
}

// A method that tries to recover from panics, and if it succeeds, calls the given onPanic function with an error that
// explains the cause of the panic. This function should only be called from a defer statement.
func Recover(onPanic func(cause error)) {
	if rec := recover(); rec != nil {
		err, isError := rec.(error)
		if !isError {
			err = fmt.Errorf("%v", rec)
		}
		onPanic(WithStackTrace(err))
	}
}

// Use this to wrap every command you add to *cli.App to handle panics by logging them with a stack trace and returning
// an error up the chain.
func WithPanicHandling(action func(c *cli.Context) error) func(c *cli.Context) error {
	return func(context *cli.Context) (err error) {
		defer Recover(func(cause error) {
			err = cause
		})

		return action(context)
	}
}
