package errors

import (
	"errors"
	"fmt"
	"io"
	"runtime"
)

// Fields is a collection of key value pairs that can be attached to an error to improve debugging.
type Fields map[string]interface{}

// New returns an error that has a stack trace, and an optional set of fields.
func New(msg string, fields ...Fields) error {
	return newDebugError(nil, msg, fields, 1)
}

// Wrap creates an error that has an inner error, a stack trace, and an optional set of fields.
// Use Wrap to "add" a stack trace to an error that doesn't have one.
func Wrap(err error, msg string, fields ...Fields) error {
	if err == nil {
		return nil
	}

	return newDebugError(err, msg, fields, 1)
}

// Cause returns a wrapped error's underlying error, or nil if there is no underlying error.
// NOTE: Cause follows rollbar/rollbar-go's semantics, instead of error/pkg's semantics of returning the
// first error in the chain that doesnt implement Causer().
func Cause(err error) error {
	if err == nil {
		return nil
	}

	if errWithCause, ok := err.(Causer); ok {
		return errWithCause.Cause()
	}

	return nil
}

// Unwrap returns the result of calling the Unwrap method on err, if err's type contains an Unwrap
// method returning error. Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
	return errors.Unwrap(err)
}

// As finds the first error in err's chain that matches target, and if so, sets target to that error
// value and returns true.
//
// The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling
// Unwrap.
//
// An error matches target if the error's concrete value is assignable to the value pointed to by target,
// or if the error has a method As(interface{}) bool such that As(target) returns true. In the latter case,
// the As method is responsible for setting target.
//
// As will panic if target is not a non-nil pointer to either a type that implements error, or to any
// interface type. As returns false if err is nil.
func As(err error, target interface{}) bool {
	return errors.As(err, target)
}

// Is reports whether any error in err's chain matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by repeatedly
// calling Unwrap.
//
//An error is considered to match a target if it is equal to that target or if it implements
// a method Is(error) bool such that Is(target) returns true.
func Is(err, target error) bool {
	return errors.Is(err, target)
}

// UnwrapAll unwraps the given error, and returns the innermost error.
func UnwrapAll(err error) error {
	if err == nil {
		return nil
	}

	for {
		errWithCause, ok := err.(Causer)
		if !ok {
			return err
		}

		inner := errWithCause.Cause()
		if inner == nil {
			return err
		}

		err = inner
	}
}

// Message returns a wrapped error's own message instead of a message made up of all errors in the chain.
func Message(err error) string {
	if err == nil {
		return ""
	}

	errWithMessage, ok := err.(Messager)
	if !ok {
		return ""
	}

	return errWithMessage.Message()
}

// Frames returns an error's stack trace, if it implements Stacker, or nil otherwise.
func Frames(err error) []runtime.Frame {
	if err == nil {
		return nil
	}

	errWithCause, ok := err.(Stacker)
	if !ok {
		return nil
	}

	return errWithCause.Stack()
}

// GetFields returns an error's fields if it implements Fieldser, or nil otherwise.
func GetFields(err error) map[string]interface{} {
	if err == nil {
		return nil
	}

	errWithFields, ok := err.(Fieldser)
	if !ok {
		return nil
	}

	return errWithFields.Fields()
}

// Causer supports retrieving an error's underlying error.
type Causer interface {
	error
	Cause() error
}

// Stacker supports retrieving an error's stack trace.
type Stacker interface {
	error
	Stack() []runtime.Frame
}

// Fieldser allows a caller get fetch a map of fields that are attached to an error.
type Fieldser interface {
	error
	Fields() map[string]interface{}
}

// Messager allows a caller to fetch the message that an error was created with,.
type Messager interface {
	error
	Message() string
}

func newDebugError(causeErr error, msg string, fields []Fields, additionalSkip int) error {
	mergedFields := merge(fields)

	frames := getCallersFrames(additionalSkip + 1)

	return &debugError{
		message: msg,
		fields:  mergedFields,
		frames:  frames,
		cause:   causeErr,
	}
}

func getCallersFrames(skip int) []runtime.Frame {
	programCounters := make([]uintptr, 100)
	numCounters := runtime.Callers(2+skip, programCounters)

	framesList := runtime.CallersFrames(programCounters)
	frames := make([]runtime.Frame, 0, numCounters)

	for {
		frame, more := framesList.Next()
		if frame != (runtime.Frame{}) {
			frames = append(frames, frame)
		}

		if !more {
			break
		}
	}

	return frames
}

type debugError struct {
	message string
	frames  []runtime.Frame
	cause   error
	fields  map[string]interface{}
}

var _ error = &debugError{}
var _ Causer = &debugError{}
var _ Stacker = &debugError{}
var _ Messager = &debugError{}

func (e debugError) Error() string {
	if e.cause == nil {
		return e.message
	}
	return fmt.Sprintf("%s: %s", e.message, e.cause.Error())
}

func (e debugError) Cause() error {
	return e.cause
}

func (e debugError) Unwrap() error {
	return e.cause
}

func (e debugError) Message() string {
	return e.message
}

func (e debugError) Fields() map[string]interface{} {
	return e.fields
}

func (e debugError) Stack() []runtime.Frame {
	return e.frames
}

func (e *debugError) String() string {
	return e.Error()
}

// Format defines the output for various print formats:
//    "%v" and "%s" prints only the error message
// 	  "%q" prints an escaped error message
//    "+v" prints the error message with the call stack and fields pretty-printed
func (e *debugError) Format(s fmt.State, verb rune) {
	switch verb {
	case 'v':
		if s.Flag('+') {
			e.writeVerbose(s)
		} else {
			_, _ = fmt.Fprint(s, e.Error())
		}

	case 's':
		_, _ = fmt.Fprint(s, e.Error())

	case 'q':
		_, _ = fmt.Fprintf(s, "%q", e.Error())
	}
}

func (e *debugError) writeVerbose(w io.Writer) {
	_, _ = fmt.Fprint(w, e.Error())
	_, _ = fmt.Fprintf(w, "\n")

	if len(e.fields) > 0 {
		for k, v := range e.fields {
			_, _ = fmt.Fprintf(w, "%s=\"%v\" ", k, v)
		}
		_, _ = fmt.Fprintln(w)
	}

	for _, stack := range e.frames {
		_, _ = fmt.Fprintln(w, stack.Function)
		_, _ = fmt.Fprintf(w, "\t%s:%d\n", stack.File, stack.Line)
	}
}

func merge(fieldMaps []Fields) map[string]interface{} {
	if len(fieldMaps) == 0 {
		return nil
	} else if len(fieldMaps) == 1 {
		return fieldMaps[0]
	}

	merged := make(map[string]interface{})
	for _, fieldMap := range fieldMaps {
		for k, v := range fieldMap {
			merged[k] = v
		}
	}
	return merged
}
