package errortracking

import (
	"reflect"

	"github.com/getsentry/sentry-go"
)

// maxErrorDepth is the maximum number of errors reported in a chain of errors.
// This protects the SDK from an arbitrarily long chain of wrapped errors.
//
// An additional consideration is that arguably reporting a long chain of errors
// is of little use when debugging production errors with Sentry. The Sentry UI
// is not optimized for long chains either. The top-level error together with a
// stack trace is often the most useful information.
const maxErrorDepth = 10

type hub interface {
	CaptureEvent(event *sentry.Event) *sentry.EventID
}

type sentryTracker struct {
	hub hub
}

func newSentryTracker(hub hub) *sentryTracker {
	return &sentryTracker{
		hub: hub,
	}
}

func (s *sentryTracker) Capture(err error, opts ...CaptureOption) {
	cfg, event := applyCaptureOptions(opts)

	if cfg.attachStackTrace {
		attachExceptions(event, err)
	} else {
		singleException(event, err)
	}

	s.hub.CaptureEvent(event)
}

func singleException(event *sentry.Event, err error) {
	event.Exception = []sentry.Exception{
		{
			Type:       reflect.TypeOf(err).String(),
			Value:      err.Error(),
			Stacktrace: sentry.ExtractStacktrace(err),
		},
	}
}

// attachExceptions is a modified copy of https://github.com/getsentry/sentry-go/blob/d5877e5a5ddc56d2fd9b48b7a5162a5d7ee47cd8/client.go
// which allows the stack trace with maxErrorDepth to be attached. This allows the event to keep the CaptureOptions
// in place, as using sentry.CaptureException would remove those fields.
func attachExceptions(event *sentry.Event, err error) {
	if err == nil {
		return
	}

	for i := 0; i < maxErrorDepth && err != nil; i++ {
		event.Exception = append(event.Exception, sentry.Exception{
			Value:      err.Error(),
			Type:       reflect.TypeOf(err).String(),
			Stacktrace: sentry.ExtractStacktrace(err),
		})
		switch previous := err.(type) {
		case interface{ Unwrap() error }:
			err = previous.Unwrap()
		case interface{ Cause() error }:
			err = previous.Cause()
		default:
			err = nil
		}
	}

	// Add a trace of the current stack to the most recent error in a chain if
	// it doesn't have a stack trace yet.
	// We only add to the most recent error to avoid duplication and because the
	// current stack is most likely unrelated to errors deeper in the chain.
	if event.Exception[0].Stacktrace == nil {
		event.Exception[0].Stacktrace = sentry.NewStacktrace()
	}

	// event.Exception should be sorted such that the most recent error is last.
	reverse(event.Exception)
}

// reverse reverses the slice a in place.
func reverse(a []sentry.Exception) {
	for i := len(a)/2 - 1; i >= 0; i-- {
		opp := len(a) - 1 - i
		a[i], a[opp] = a[opp], a[i]
	}
}
