package oops

import (
	"fmt"

	"github.com/go-stack/stack"
	"github.com/rs/zerolog"
)

type Error struct {
	Message string
	Wrapped error
	Stack   CallStack
}

func (e *Error) Error() string {
	if e.Wrapped == nil {
		return e.Message
	} else {
		return fmt.Sprintf("%s: %v", e.Message, e.Wrapped)
	}
}

func (e *Error) Unwrap() error {
	return e.Wrapped
}

type CallStack []StackFrame

func (s CallStack) MarshalZerologArray(a *zerolog.Array) {
	for _, frame := range s {
		a.Object(frame)
	}
}

type StackFrame struct {
	File     string `json:"file"`
	Line     int    `json:"line"`
	Function string `json:"function"`
}

func (f StackFrame) MarshalZerologObject(e *zerolog.Event) {
	e.
		Str("file", f.File).
		Int("line", f.Line).
		Str("function", f.Function)
}

var ZerologStackMarshaler = func(err error) interface{} {
	if asOops, ok := err.(*Error); ok {
		return asOops.Stack
	}
	// NOTE(asaf): If we got here, it means zerolog is trying to output a non-oops error.
	//			   We remove this call and the zerolog caller from the stack.
	return Trace()[2:]
}

func New(wrapped error, format string, args ...interface{}) error {
	return &Error{
		Message: fmt.Sprintf(format, args...),
		Wrapped: wrapped,
		Stack:   Trace()[1:], // NOTE(asaf): Remove the call to New from the stack
	}
}

func Trace() CallStack {
	trace := stack.Trace().TrimRuntime()[1:]
	frames := make(CallStack, len(trace))
	for i, call := range trace {
		callFrame := call.Frame()
		frames[i] = StackFrame{
			File:     callFrame.File,
			Line:     callFrame.Line,
			Function: callFrame.Function,
		}
	}

	return frames
}
