package sentry

import (
	"context"
	"fmt"
	"sync"
	"time"

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

type contextKey int

// Keys used to store values in a Context. Use with Context.Value to access
// values stored by the SDK.
const (
	// HubContextKey is the key used to store the current Hub.
	HubContextKey = contextKey(1)
	// RequestContextKey is the key used to store the current http.Request.
	RequestContextKey = contextKey(2)
)

// currentHub is the initial Hub with no Client bound and an empty Scope.
var currentHub = NewHub(nil, NewScope())

// Hub is the central object that manages scopes and clients.
//
// This can be used to capture events and manage the scope.
// The default hub that is available automatically.
//
// In most situations developers do not need to interface the hub. Instead
// toplevel convenience functions are exposed that will automatically dispatch
// to global (CurrentHub) hub.  In some situations this might not be
// possible in which case it might become necessary to manually work with the
// hub. This is for instance the case when working with async code.
type Hub struct {
	mu          sync.RWMutex
	stack       *stack
	lastEventID EventID
}

type layer struct {
	// mu protects concurrent reads and writes to client.
	mu     sync.RWMutex
	client *Client
	// scope is read-only, not protected by mu.
	scope *Scope
}

// Client returns the layer's client. Safe for concurrent use.
func (l *layer) Client() *Client {
	l.mu.RLock()
	defer l.mu.RUnlock()
	return l.client
}

// SetClient sets the layer's client. Safe for concurrent use.
func (l *layer) SetClient(c *Client) {
	l.mu.Lock()
	defer l.mu.Unlock()
	l.client = c
}

type stack []*layer

// NewHub returns an instance of a Hub with provided Client and Scope bound.
func NewHub(client *Client, scope *Scope) *Hub {
	hub := Hub{
		stack: &stack{{
			client: client,
			scope:  scope,
		}},
	}
	return &hub
}

// CurrentHub returns an instance of previously initialized Hub stored in the global namespace.
func CurrentHub() *Hub {
	return currentHub
}

// LastEventID returns the ID of the last event (error or message) captured
// through the hub and sent to the underlying transport.
//
// Transactions and events dropped by sampling or event processors do not change
// the last event ID.
//
// LastEventID is a convenience method to cover use cases in which errors are
// captured indirectly and the ID is needed. For example, it can be used as part
// of an HTTP middleware to log the ID of the last error, if any.
//
// For more flexibility, consider instead using the ClientOptions.BeforeSend
// function or event processors.
func (hub *Hub) LastEventID() EventID {
	hub.mu.RLock()
	defer hub.mu.RUnlock()

	return hub.lastEventID
}

// stackTop returns the top layer of the hub stack. Valid hubs always have at
// least one layer, therefore stackTop always return a non-nil pointer.
func (hub *Hub) stackTop() *layer {
	hub.mu.RLock()
	defer hub.mu.RUnlock()

	stack := hub.stack
	stackLen := len(*stack)
	top := (*stack)[stackLen-1]
	return top
}

// Clone returns a copy of the current Hub with top-most scope and client copied over.
func (hub *Hub) Clone() *Hub {
	top := hub.stackTop()
	scope := top.scope
	if scope != nil {
		scope = scope.Clone()
	}
	return NewHub(top.Client(), scope)
}

// Scope returns top-level Scope of the current Hub or nil if no Scope is bound.
func (hub *Hub) Scope() *Scope {
	top := hub.stackTop()
	return top.scope
}

// Client returns top-level Client of the current Hub or nil if no Client is bound.
func (hub *Hub) Client() *Client {
	top := hub.stackTop()
	return top.Client()
}

// PushScope pushes a new scope for the current Hub and reuses previously bound Client.
func (hub *Hub) PushScope() *Scope {
	top := hub.stackTop()

	var scope *Scope
	if top.scope != nil {
		scope = top.scope.Clone()
	} else {
		scope = NewScope()
	}

	hub.mu.Lock()
	defer hub.mu.Unlock()

	*hub.stack = append(*hub.stack, &layer{
		client: top.Client(),
		scope:  scope,
	})

	return scope
}

// PopScope drops the most recent scope.
//
// Calls to PopScope must be coordinated with PushScope. For most cases, using
// WithScope should be more convenient.
//
// Calls to PopScope that do not match previous calls to PushScope are silently
// ignored.
func (hub *Hub) PopScope() {
	hub.mu.Lock()
	defer hub.mu.Unlock()

	stack := *hub.stack
	stackLen := len(stack)
	if stackLen > 1 {
		// Never pop the last item off the stack, the stack should always have
		// at least one item.
		*hub.stack = stack[0 : stackLen-1]
	}
}

// BindClient binds a new Client for the current Hub.
func (hub *Hub) BindClient(client *Client) {
	top := hub.stackTop()
	top.SetClient(client)
}

// WithScope runs f in an isolated temporary scope.
//
// It is useful when extra data should be sent with a single capture call, for
// instance a different level or tags.
//
// The scope passed to f starts as a clone of the current scope and can be
// freely modified without affecting the current scope.
//
// It is a shorthand for PushScope followed by PopScope.
func (hub *Hub) WithScope(f func(scope *Scope)) {
	scope := hub.PushScope()
	defer hub.PopScope()
	f(scope)
}

// ConfigureScope runs f in the current scope.
//
// It is useful to set data that applies to all events that share the current
// scope.
//
// Modifying the scope affects all references to the current scope.
//
// See also WithScope for making isolated temporary changes.
func (hub *Hub) ConfigureScope(f func(scope *Scope)) {
	scope := hub.Scope()
	f(scope)
}

// CaptureEvent calls the method of a same name on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Scope or Client available.
func (hub *Hub) CaptureEvent(event *Event) *EventID {
	client, scope := hub.Client(), hub.Scope()
	if client == nil || scope == nil {
		return nil
	}
	eventID := client.CaptureEvent(event, nil, scope)

	if event.Type != transactionType && eventID != nil {
		hub.mu.Lock()
		hub.lastEventID = *eventID
		hub.mu.Unlock()
	}
	return eventID
}

// CaptureMessage calls the method of a same name on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Scope or Client available.
func (hub *Hub) CaptureMessage(message string) *EventID {
	client, scope := hub.Client(), hub.Scope()
	if client == nil || scope == nil {
		return nil
	}
	eventID := client.CaptureMessage(message, nil, scope)

	if eventID != nil {
		hub.mu.Lock()
		hub.lastEventID = *eventID
		hub.mu.Unlock()
	}
	return eventID
}

// CaptureException calls the method of a same name on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Scope or Client available.
func (hub *Hub) CaptureException(exception error) *EventID {
	client, scope := hub.Client(), hub.Scope()
	if client == nil || scope == nil {
		return nil
	}
	eventID := client.CaptureException(exception, &EventHint{OriginalException: exception}, scope)

	if eventID != nil {
		hub.mu.Lock()
		hub.lastEventID = *eventID
		hub.mu.Unlock()
	}
	return eventID
}

// CaptureCheckIn calls the method of the same name on currently bound Client instance
// passing it a top-level Scope.
// Returns CheckInID if the check-in was captured successfully, or nil otherwise.
func (hub *Hub) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID {
	client, scope := hub.Client(), hub.Scope()
	if client == nil {
		return nil
	}

	return client.CaptureCheckIn(checkIn, monitorConfig, scope)
}

// AddBreadcrumb records a new breadcrumb.
//
// The total number of breadcrumbs that can be recorded are limited by the
// configuration on the client.
func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
	client := hub.Client()

	// If there's no client, just store it on the scope straight away
	if client == nil {
		hub.Scope().AddBreadcrumb(breadcrumb, defaultMaxBreadcrumbs)
		return
	}

	limit := client.options.MaxBreadcrumbs
	switch {
	case limit < 0:
		return
	case limit == 0:
		limit = defaultMaxBreadcrumbs
	}

	if client.options.BeforeBreadcrumb != nil {
		if hint == nil {
			hint = &BreadcrumbHint{}
		}
		if breadcrumb = client.options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil {
			debuglog.Println("breadcrumb dropped due to BeforeBreadcrumb callback.")
			return
		}
	}

	hub.Scope().AddBreadcrumb(breadcrumb, limit)
}

// Recover calls the method of a same name on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Scope or Client available.
func (hub *Hub) Recover(err interface{}) *EventID {
	if err == nil {
		err = recover()
	}
	client, scope := hub.Client(), hub.Scope()
	if client == nil || scope == nil {
		return nil
	}
	return client.Recover(err, &EventHint{RecoveredException: err}, scope)
}

// RecoverWithContext calls the method of a same name on currently bound Client instance
// passing it a top-level Scope.
// Returns EventID if successfully, or nil if there's no Scope or Client available.
func (hub *Hub) RecoverWithContext(ctx context.Context, err interface{}) *EventID {
	if err == nil {
		err = recover()
	}
	client, scope := hub.Client(), hub.Scope()
	if client == nil || scope == nil {
		return nil
	}
	return client.RecoverWithContext(ctx, err, &EventHint{RecoveredException: err}, scope)
}

// Flush waits until the underlying Transport sends any buffered events to the
// Sentry server, blocking for at most the given timeout. It returns false if
// the timeout was reached. In that case, some events may not have been sent.
//
// Flush should be called before terminating the program to avoid
// unintentionally dropping events.
//
// Do not call Flush indiscriminately after every call to CaptureEvent,
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
// the network synchronously, configure it to use the HTTPSyncTransport in the
// call to Init.
func (hub *Hub) Flush(timeout time.Duration) bool {
	client := hub.Client()

	if client == nil {
		return false
	}

	return client.Flush(timeout)
}

// FlushWithContext waits until the underlying Transport sends any buffered events
// to the Sentry server, blocking for at most the duration specified by the context.
// It returns false if the context is canceled before the events are sent. In such a case,
// some events may not be delivered.
//
// FlushWithContext should be called before terminating the program to ensure no
// events are unintentionally dropped.
//
// Avoid calling FlushWithContext indiscriminately after each call to CaptureEvent,
// CaptureException, or CaptureMessage. To send events synchronously over the network,
// configure the SDK to use HTTPSyncTransport during initialization with Init.

func (hub *Hub) FlushWithContext(ctx context.Context) bool {
	client := hub.Client()

	if client == nil {
		return false
	}

	return client.FlushWithContext(ctx)
}

// GetTraceparent returns the current Sentry traceparent string, to be used as a HTTP header value
// or HTML meta tag value.
// This function is context aware, as in it either returns the traceparent based
// on the current span, or the scope's propagation context.
func (hub *Hub) GetTraceparent() string {
	scope := hub.Scope()

	if scope.span != nil {
		return scope.span.ToSentryTrace()
	}

	return fmt.Sprintf("%s-%s", scope.propagationContext.TraceID, scope.propagationContext.SpanID)
}

// GetTraceparentW3C returns the current traceparent string in W3C format.
// This is intended for propagation to downstream services that expect the W3C header.
func (hub *Hub) GetTraceparentW3C() string {
	scope := hub.Scope()
	if scope.span != nil {
		return scope.span.ToTraceparent()
	}

	return fmt.Sprintf("00-%s-%s-00", scope.propagationContext.TraceID, scope.propagationContext.SpanID)
}

// GetBaggage returns the current Sentry baggage string, to be used as a HTTP header value
// or HTML meta tag value.
// This function is context aware, as in it either returns the baggage based
// on the current span or the scope's propagation context.
func (hub *Hub) GetBaggage() string {
	scope := hub.Scope()

	if scope.span != nil {
		return scope.span.ToBaggage()
	}

	return scope.propagationContext.DynamicSamplingContext.String()
}

// HasHubOnContext checks whether Hub instance is bound to a given Context struct.
func HasHubOnContext(ctx context.Context) bool {
	_, ok := ctx.Value(HubContextKey).(*Hub)
	return ok
}

// GetHubFromContext tries to retrieve Hub instance from the given Context struct
// or return nil if one is not found.
func GetHubFromContext(ctx context.Context) *Hub {
	if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
		return hub
	}
	return nil
}

// hubFromContext returns either a hub stored in the context or the current hub.
// The return value is guaranteed to be non-nil, unlike GetHubFromContext.
func hubFromContext(ctx context.Context) *Hub {
	if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
		return hub
	}
	return currentHub
}

// SetHubOnContext stores given Hub instance on the Context struct and returns a new Context.
func SetHubOnContext(ctx context.Context, hub *Hub) context.Context {
	return context.WithValue(ctx, HubContextKey, hub)
}
