package sentry

import (
	"bytes"
	"context"
	"io"
	"maps"
	"net/http"
	"sync"
	"time"

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

// Scope holds contextual data for the current scope.
//
// The scope is an object that can cloned efficiently and stores data that is
// locally relevant to an event. For instance the scope will hold recorded
// breadcrumbs and similar information.
//
// The scope can be interacted with in two ways. First, the scope is routinely
// updated with information by functions such as AddBreadcrumb which will modify
// the current scope. Second, the current scope can be configured through the
// ConfigureScope function or Hub method of the same name.
//
// The scope is meant to be modified but not inspected directly. When preparing
// an event for reporting, the current client adds information from the current
// scope into the event.
type Scope struct {
	mu          sync.RWMutex
	attributes  map[string]attribute.Value
	breadcrumbs []*Breadcrumb
	attachments []*Attachment
	user        User
	tags        map[string]string
	contexts    map[string]Context
	fingerprint []string
	level       Level
	request     *http.Request
	// requestBody holds a reference to the original request.Body.
	requestBody interface {
		// Bytes returns bytes from the original body, lazily buffered as the
		// original body is read.
		Bytes() []byte
		// Overflow returns true if the body is larger than the maximum buffer
		// size.
		Overflow() bool
	}
	eventProcessors []EventProcessor

	propagationContext PropagationContext
	span               *Span
}

// NewScope creates a new Scope.
func NewScope() *Scope {
	return &Scope{
		attributes:         make(map[string]attribute.Value),
		breadcrumbs:        make([]*Breadcrumb, 0),
		attachments:        make([]*Attachment, 0),
		tags:               make(map[string]string),
		contexts:           make(map[string]Context),
		fingerprint:        make([]string, 0),
		propagationContext: NewPropagationContext(),
	}
}

// AddBreadcrumb adds new breadcrumb to the current scope
// and optionally throws the old one if limit is reached.
func (scope *Scope) AddBreadcrumb(breadcrumb *Breadcrumb, limit int) {
	if breadcrumb.Timestamp.IsZero() {
		breadcrumb.Timestamp = time.Now()
	}

	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.breadcrumbs = append(scope.breadcrumbs, breadcrumb)
	if len(scope.breadcrumbs) > limit {
		scope.breadcrumbs = scope.breadcrumbs[1 : limit+1]
	}
}

// ClearBreadcrumbs clears all breadcrumbs from the current scope.
func (scope *Scope) ClearBreadcrumbs() {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.breadcrumbs = []*Breadcrumb{}
}

// AddAttachment adds new attachment to the current scope.
func (scope *Scope) AddAttachment(attachment *Attachment) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.attachments = append(scope.attachments, attachment)
}

// ClearAttachments clears all attachments from the current scope.
func (scope *Scope) ClearAttachments() {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.attachments = []*Attachment{}
}

// SetUser sets the user for the current scope.
func (scope *Scope) SetUser(user User) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.user = user
}

// SetRequest sets the request for the current scope.
func (scope *Scope) SetRequest(r *http.Request) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.request = r

	if r == nil {
		return
	}

	// Don't buffer request body if we know it is oversized.
	if r.ContentLength > maxRequestBodyBytes {
		return
	}
	// Don't buffer if there is no body.
	if r.Body == nil || r.Body == http.NoBody {
		return
	}
	buf := &limitedBuffer{Capacity: maxRequestBodyBytes}
	r.Body = readCloser{
		Reader: io.TeeReader(r.Body, buf),
		Closer: r.Body,
	}
	scope.requestBody = buf
}

// SetRequestBody sets the request body for the current scope.
//
// This method should only be called when the body bytes are already available
// in memory. Typically, the request body is buffered lazily from the
// Request.Body from SetRequest.
func (scope *Scope) SetRequestBody(b []byte) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	capacity := maxRequestBodyBytes
	overflow := false
	if len(b) > capacity {
		overflow = true
		b = b[:capacity]
	}
	scope.requestBody = &limitedBuffer{
		Capacity: capacity,
		Buffer:   *bytes.NewBuffer(b),
		overflow: overflow,
	}
}

// maxRequestBodyBytes is the default maximum request body size to send to
// Sentry.
const maxRequestBodyBytes = 10 * 1024

// A limitedBuffer is like a bytes.Buffer, but limited to store at most Capacity
// bytes. Any writes past the capacity are silently discarded, similar to
// io.Discard.
type limitedBuffer struct {
	Capacity int

	bytes.Buffer
	overflow bool
}

// Write implements io.Writer.
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
	// Silently ignore writes after overflow.
	if b.overflow {
		return len(p), nil
	}
	left := b.Capacity - b.Len()
	if left < 0 {
		left = 0
	}
	if len(p) > left {
		b.overflow = true
		p = p[:left]
	}
	return b.Buffer.Write(p)
}

// Overflow returns true if the limitedBuffer discarded bytes written to it.
func (b *limitedBuffer) Overflow() bool {
	return b.overflow
}

// readCloser combines an io.Reader and an io.Closer to implement io.ReadCloser.
type readCloser struct {
	io.Reader
	io.Closer
}

// SetAttributes adds attributes to the current scope.
func (scope *Scope) SetAttributes(attrs ...attribute.Builder) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	for _, a := range attrs {
		if a.Value.Type() == attribute.INVALID {
			debuglog.Printf("invalid attribute: %v", a)
			continue
		}
		scope.attributes[a.Key] = a.Value
	}
}

// RemoveAttribute removes an attribute from the current scope.
func (scope *Scope) RemoveAttribute(key string) {
	scope.mu.Lock()
	defer scope.mu.Unlock()
	delete(scope.attributes, key)
}

// SetTag adds a tag to the current scope.
func (scope *Scope) SetTag(key, value string) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.tags[key] = value
}

// SetTags assigns multiple tags to the current scope.
func (scope *Scope) SetTags(tags map[string]string) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	for k, v := range tags {
		scope.tags[k] = v
	}
}

// RemoveTag removes a tag from the current scope.
func (scope *Scope) RemoveTag(key string) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	delete(scope.tags, key)
}

// SetContext adds a context to the current scope.
func (scope *Scope) SetContext(key string, value Context) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.contexts[key] = value
}

// SetContexts assigns multiple contexts to the current scope.
func (scope *Scope) SetContexts(contexts map[string]Context) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	for k, v := range contexts {
		scope.contexts[k] = v
	}
}

// RemoveContext removes a context from the current scope.
func (scope *Scope) RemoveContext(key string) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	delete(scope.contexts, key)
}

// SetFingerprint sets new fingerprint for the current scope.
func (scope *Scope) SetFingerprint(fingerprint []string) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.fingerprint = fingerprint
}

// SetLevel sets new level for the current scope.
func (scope *Scope) SetLevel(level Level) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.level = level
}

// SetPropagationContext sets the propagation context for the current scope.
func (scope *Scope) SetPropagationContext(propagationContext PropagationContext) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.propagationContext = propagationContext
}

func (scope *Scope) propagationContextSnapshot() PropagationContext {
	scope.mu.RLock()
	defer scope.mu.RUnlock()

	return scope.propagationContext
}

// GetSpan returns the span from the current scope.
func (scope *Scope) GetSpan() *Span {
	scope.mu.RLock()
	defer scope.mu.RUnlock()

	return scope.span
}

// SetSpan sets a span for the current scope.
func (scope *Scope) SetSpan(span *Span) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.span = span
}

// Clone returns a copy of the current scope with all data copied over.
func (scope *Scope) Clone() *Scope {
	scope.mu.RLock()
	defer scope.mu.RUnlock()

	clone := NewScope()
	clone.user = scope.user
	clone.breadcrumbs = make([]*Breadcrumb, len(scope.breadcrumbs))
	copy(clone.breadcrumbs, scope.breadcrumbs)
	clone.attachments = make([]*Attachment, len(scope.attachments))
	copy(clone.attachments, scope.attachments)
	clone.attributes = maps.Clone(scope.attributes)
	clone.contexts = maps.Clone(scope.contexts)
	clone.tags = maps.Clone(scope.tags)
	clone.fingerprint = make([]string, len(scope.fingerprint))
	copy(clone.fingerprint, scope.fingerprint)
	clone.level = scope.level
	clone.request = scope.request
	clone.requestBody = scope.requestBody
	clone.eventProcessors = scope.eventProcessors
	clone.propagationContext = scope.propagationContext
	clone.span = scope.span
	return clone
}

// Clear removes the data from the current scope. Not safe for concurrent use.
func (scope *Scope) Clear() {
	*scope = *NewScope()
}

// AddEventProcessor adds an event processor to the current scope.
func (scope *Scope) AddEventProcessor(processor EventProcessor) {
	scope.mu.Lock()
	defer scope.mu.Unlock()

	scope.eventProcessors = append(scope.eventProcessors, processor)
}

// ApplyToEvent takes the data from the current scope and attaches it to the event.
func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client) *Event { //nolint:gocyclo
	scope.mu.RLock()
	defer scope.mu.RUnlock()

	if len(scope.breadcrumbs) > 0 {
		event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...)
	}

	if len(scope.attachments) > 0 {
		event.Attachments = append(event.Attachments, scope.attachments...)
	}

	if len(scope.tags) > 0 {
		if event.Tags == nil {
			event.Tags = make(map[string]string, len(scope.tags))
		}

		for key, value := range scope.tags {
			event.Tags[key] = value
		}
	}

	if len(scope.contexts) > 0 {
		if event.Contexts == nil {
			event.Contexts = make(map[string]Context)
		}

		for key, value := range scope.contexts {
			if key == "trace" && event.Type == transactionType {
				// Do not override trace context of
				// transactions, otherwise it breaks the
				// transaction event representation.
				// For error events, the trace context is used
				// to link errors and traces/spans in Sentry.
				continue
			}

			// Ensure we are not overwriting event fields
			if _, ok := event.Contexts[key]; !ok {
				event.Contexts[key] = cloneContext(value)
			}
		}
	}

	if event.Contexts == nil {
		event.Contexts = make(map[string]Context)
	}

	if scope.span != nil {
		if _, ok := event.Contexts["trace"]; !ok {
			event.Contexts["trace"] = scope.span.traceContext().Map()
		}

		transaction := scope.span.GetTransaction()
		if transaction != nil {
			event.sdkMetaData.dsc = DynamicSamplingContextFromTransaction(transaction)
		}
	} else {
		event.Contexts["trace"] = scope.propagationContext.Map()

		dsc := scope.propagationContext.DynamicSamplingContext
		if !dsc.HasEntries() && client != nil {
			dsc = DynamicSamplingContextFromScope(scope, client)
		}
		event.sdkMetaData.dsc = dsc
	}

	// If an external trace resolver is registered (e.g. OTel), override
	// trace/span IDs from the hint context or the scope's request context.
	if client != nil {
		var ctx context.Context
		if hint != nil {
			ctx = hint.Context
		}
		if ctx == nil && scope.request != nil {
			ctx = scope.request.Context()
		}
		if traceID, spanID, ok := client.externalTraceContextFromContext(ctx); event.Type != transactionType && ok {
			traceCtx := event.Contexts["trace"]
			traceCtx["trace_id"] = traceID.String()
			traceCtx["span_id"] = spanID.String()
		}
	}

	if event.User.IsEmpty() {
		event.User = scope.user
	}

	if len(event.Fingerprint) == 0 {
		event.Fingerprint = append(event.Fingerprint, scope.fingerprint...)
	}

	if scope.level != "" {
		event.Level = scope.level
	}

	if event.Request == nil && scope.request != nil {
		event.Request = newRequest(scope.request, sendDefaultPIIEnabled(client))
		// NOTE: The SDK does not attempt to send partial request body data.
		//
		// The reason being that Sentry's ingest pipeline and UI are optimized
		// to show structured data. Additionally, tooling around PII scrubbing
		// relies on structured data; truncated request bodies would create
		// invalid payloads that are more prone to leaking PII data.
		//
		// Users can still send more data along their events if they want to,
		// for example using Event.Contexts.
		if scope.requestBody != nil && !scope.requestBody.Overflow() {
			event.Request.Data = string(scope.requestBody.Bytes())
		}
	}

	for _, processor := range scope.eventProcessors {
		id := event.EventID
		category := event.toCategory()
		spanCountBefore := event.GetSpanCount()
		event = processor(event, hint)
		if event == nil {
			debuglog.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id)
			if client != nil {
				client.reportRecorder.RecordOne(report.ReasonEventProcessor, category)
				if category == ratelimit.CategoryTransaction {
					client.reportRecorder.Record(report.ReasonEventProcessor, ratelimit.CategorySpan, int64(spanCountBefore))
				}
			}
			return nil
		}
		if droppedSpans := spanCountBefore - event.GetSpanCount(); droppedSpans > 0 {
			if client != nil {
				client.reportRecorder.Record(report.ReasonEventProcessor, ratelimit.CategorySpan, int64(droppedSpans))
			}
		}
	}

	return event
}

// cloneContext returns a new context with keys and values copied from the passed one.
//
// Note: a new Context (map) is returned, but the function does NOT do
// a proper deep copy: if some context values are pointer types (e.g. maps),
// they won't be properly copied.
func cloneContext(c Context) Context {
	res := make(Context, len(c))
	for k, v := range c {
		res[k] = v
	}
	return res
}

func (scope *Scope) populateAttrs(attrs map[string]attribute.Value) {
	if scope == nil {
		return
	}

	scope.mu.RLock()
	defer scope.mu.RUnlock()

	// Add user-related attributes
	if !scope.user.IsEmpty() {
		if scope.user.ID != "" {
			attrs["user.id"] = attribute.StringValue(scope.user.ID)
		}
		if scope.user.Name != "" {
			attrs["user.name"] = attribute.StringValue(scope.user.Name)
		}
		if scope.user.Email != "" {
			attrs["user.email"] = attribute.StringValue(scope.user.Email)
		}
	}

	for k, v := range scope.attributes {
		attrs[k] = v
	}
}

// hubFromContexts is a helper to return the first hub found in the given contexts.
func hubFromContexts(ctxs ...context.Context) *Hub {
	for _, ctx := range ctxs {
		if ctx == nil {
			continue
		}
		if hub := GetHubFromContext(ctx); hub != nil {
			return hub
		}
	}
	return nil
}

// resolveTrace resolves trace ID and span ID from the given scope and contexts.
//
// The resolution order follows a most-specific-to-least-specific pattern:
//  1. If an external trace resolver was registered (eg. OTel), we prioritise trace context
//     information from that
//  2. Check for span directly in contexts (SpanFromContext) - this is the most specific
//     source as it represents a span explicitly attached to the current operation's context
//  3. Check scope's span - provides access to span set on the hub's scope
//  4. Fall back to scope's propagation context trace ID
//
// This ordering ensures we always use the most contextually relevant tracing information.
// For example, if a specific span is active for an operation, we use that span's trace/span IDs
// rather than accidentally using a different span that might be set on the hub's scope.
func resolveTrace(scope *Scope, client *Client, ctxs ...context.Context) (traceID TraceID, spanID SpanID) {
	var span *Span

	for _, ctx := range ctxs {
		if ctx == nil {
			continue
		}
		if client != nil {
			if traceID, spanID, ok := client.externalTraceContextFromContext(ctx); ok {
				return traceID, spanID
			}
		}
		if span = SpanFromContext(ctx); span != nil {
			break
		}
	}

	if scope != nil {
		scope.mu.RLock()
		if span == nil {
			span = scope.span
		}
		if span != nil {
			traceID = span.TraceID
			spanID = span.SpanID
		} else {
			traceID = scope.propagationContext.TraceID
		}
		scope.mu.RUnlock()
	}

	return traceID, spanID
}
