package sentry

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"strings"
	"time"

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

const eventType = "event"
const transactionType = "transaction"
const checkInType = "check_in"

var logEvent = struct {
	Type        string
	ContentType string
}{
	"log",
	"application/vnd.sentry.items.log+json",
}

// Level marks the severity of the event.
type Level string

// Describes the severity of the event.
const (
	LevelDebug   Level = "debug"
	LevelInfo    Level = "info"
	LevelWarning Level = "warning"
	LevelError   Level = "error"
	LevelFatal   Level = "fatal"
)

// SdkInfo contains all metadata about the SDK.
type SdkInfo = protocol.SdkInfo
type SdkPackage = protocol.SdkPackage

// TODO: This type could be more useful, as map of interface{} is too generic
// and requires a lot of type assertions in beforeBreadcrumb calls
// plus it could just be map[string]interface{} then.

// BreadcrumbHint contains information that can be associated with a Breadcrumb.
type BreadcrumbHint map[string]interface{}

// Breadcrumb specifies an application event that occurred before a Sentry event.
// An event may contain one or more breadcrumbs.
type Breadcrumb struct {
	Type      string                 `json:"type,omitempty"`
	Category  string                 `json:"category,omitempty"`
	Message   string                 `json:"message,omitempty"`
	Data      map[string]interface{} `json:"data,omitempty"`
	Level     Level                  `json:"level,omitempty"`
	Timestamp time.Time              `json:"timestamp"`
}

// TODO: provide constants for known breadcrumb types.
// See https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types.

// MarshalJSON converts the Breadcrumb struct to JSON.
func (b *Breadcrumb) MarshalJSON() ([]byte, error) {
	// We want to omit time.Time zero values, otherwise the server will try to
	// interpret dates too far in the past. However, encoding/json doesn't
	// support the "omitempty" option for struct types. See
	// https://golang.org/issues/11939.
	//
	// We overcome the limitation and achieve what we want by shadowing fields
	// and a few type tricks.

	// breadcrumb aliases Breadcrumb to allow calling json.Marshal without an
	// infinite loop. It preserves all fields while none of the attached
	// methods.
	type breadcrumb Breadcrumb

	if b.Timestamp.IsZero() {
		return json.Marshal(struct {
			// Embed all of the fields of Breadcrumb.
			*breadcrumb
			// Timestamp shadows the original Timestamp field and is meant to
			// remain nil, triggering the omitempty behavior.
			Timestamp json.RawMessage `json:"timestamp,omitempty"`
		}{breadcrumb: (*breadcrumb)(b)})
	}
	return json.Marshal((*breadcrumb)(b))
}

// Logger provides a chaining API for structured logging to Sentry.
type Logger interface {
	// Write implements the io.Writer interface. Currently, the [sentry.Hub] is
	// context aware, in order to get the correct trace correlation. Using this
	// might result in incorrect span association on logs. If you need to use
	// Write it is recommended to create a NewLogger so that the associated context
	// is passed correctly.
	Write(p []byte) (n int, err error)

	// SetAttributes allows attaching parameters to the logger using the attribute API.
	// These attributes will be included in all subsequent log entries.
	SetAttributes(...attribute.Builder)

	// Trace defines the [sentry.LogLevel] for the log entry.
	Trace() LogEntry
	// Debug defines the [sentry.LogLevel] for the log entry.
	Debug() LogEntry
	// Info defines the [sentry.LogLevel] for the log entry.
	Info() LogEntry
	// Warn defines the [sentry.LogLevel] for the log entry.
	Warn() LogEntry
	// Error defines the [sentry.LogLevel] for the log entry.
	Error() LogEntry
	// Fatal defines the [sentry.LogLevel] for the log entry.
	Fatal() LogEntry
	// Panic defines the [sentry.LogLevel] for the log entry.
	Panic() LogEntry
	// GetCtx returns the [context.Context] set on the logger.
	GetCtx() context.Context
}

// LogEntry defines the interface for a log entry that supports chaining attributes.
type LogEntry interface {
	// WithCtx creates a new LogEntry with the specified context without overwriting the previous one.
	WithCtx(ctx context.Context) LogEntry
	// String adds a string attribute to the LogEntry.
	String(key, value string) LogEntry
	// Int adds an int attribute to the LogEntry.
	Int(key string, value int) LogEntry
	// Int64 adds an int64 attribute to the LogEntry.
	Int64(key string, value int64) LogEntry
	// Float64 adds a float64 attribute to the LogEntry.
	Float64(key string, value float64) LogEntry
	// Bool adds a bool attribute to the LogEntry.
	Bool(key string, value bool) LogEntry
	// Emit emits the LogEntry with the provided arguments.
	Emit(args ...interface{})
	// Emitf emits the LogEntry using a format string and arguments.
	Emitf(format string, args ...interface{})
}

// Attachment allows associating files with your events to aid in investigation.
// An event may contain one or more attachments.
type Attachment struct {
	Filename    string
	ContentType string
	Payload     []byte
}

// User describes the user associated with an Event. If this is used, at least
// an ID or an IP address should be provided.
type User struct {
	ID        string            `json:"id,omitempty"`
	Email     string            `json:"email,omitempty"`
	IPAddress string            `json:"ip_address,omitempty"`
	Username  string            `json:"username,omitempty"`
	Name      string            `json:"name,omitempty"`
	Data      map[string]string `json:"data,omitempty"`
}

func (u User) IsEmpty() bool {
	if u.ID != "" {
		return false
	}

	if u.Email != "" {
		return false
	}

	if u.IPAddress != "" {
		return false
	}

	if u.Username != "" {
		return false
	}

	if u.Name != "" {
		return false
	}

	if len(u.Data) > 0 {
		return false
	}

	return true
}

// Request contains information on a HTTP request related to the event.
type Request struct {
	URL         string            `json:"url,omitempty"`
	Method      string            `json:"method,omitempty"`
	Data        string            `json:"data,omitempty"`
	QueryString string            `json:"query_string,omitempty"`
	Cookies     string            `json:"cookies,omitempty"`
	Headers     map[string]string `json:"headers,omitempty"`
	Env         map[string]string `json:"env,omitempty"`
}

var sensitiveHeaders = map[string]struct{}{
	"_csrf":               {},
	"_csrf_token":         {},
	"_session":            {},
	"_xsrf":               {},
	"Api-Key":             {},
	"Apikey":              {},
	"Auth":                {},
	"Authorization":       {},
	"Cookie":              {},
	"Credentials":         {},
	"Csrf":                {},
	"Csrf-Token":          {},
	"Csrftoken":           {},
	"Ip-Address":          {},
	"Passwd":              {},
	"Password":            {},
	"Private-Key":         {},
	"Privatekey":          {},
	"Proxy-Authorization": {},
	"Remote-Addr":         {},
	"Secret":              {},
	"Session":             {},
	"Sessionid":           {},
	"Token":               {},
	"User-Session":        {},
	"X-Api-Key":           {},
	"X-Csrftoken":         {},
	"X-Forwarded-For":     {},
	"X-Real-Ip":           {},
	"XSRF-TOKEN":          {},
}

// NewRequest returns a new Sentry Request from the given http.Request.
//
// NewRequest avoids operations that depend on network access. In particular, it
// does not read r.Body.
func NewRequest(r *http.Request) *Request {
	prot := protocol.SchemeHTTP
	if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
		prot = protocol.SchemeHTTPS
	}
	url := fmt.Sprintf("%s://%s%s", prot, r.Host, r.URL.Path)

	var cookies string
	var env map[string]string
	headers := map[string]string{}

	if client := CurrentHub().Client(); client != nil && client.options.SendDefaultPII {
		// We read only the first Cookie header because of the specification:
		// https://tools.ietf.org/html/rfc6265#section-5.4
		// When the user agent generates an HTTP request, the user agent MUST NOT
		// attach more than one Cookie header field.
		cookies = r.Header.Get("Cookie")

		headers = make(map[string]string, len(r.Header))
		for k, v := range r.Header {
			headers[k] = strings.Join(v, ",")
		}

		if addr, port, err := net.SplitHostPort(r.RemoteAddr); err == nil {
			env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port}
		}
	} else {
		for k, v := range r.Header {
			if _, ok := sensitiveHeaders[k]; !ok {
				headers[k] = strings.Join(v, ",")
			}
		}
	}

	headers["Host"] = r.Host

	return &Request{
		URL:         url,
		Method:      r.Method,
		QueryString: r.URL.RawQuery,
		Cookies:     cookies,
		Headers:     headers,
		Env:         env,
	}
}

// Mechanism is the mechanism by which an exception was generated and handled.
type Mechanism struct {
	Type             string         `json:"type"`
	Description      string         `json:"description,omitempty"`
	HelpLink         string         `json:"help_link,omitempty"`
	Source           string         `json:"source,omitempty"`
	Handled          *bool          `json:"handled,omitempty"`
	ParentID         *int           `json:"parent_id,omitempty"`
	ExceptionID      int            `json:"exception_id"`
	IsExceptionGroup bool           `json:"is_exception_group,omitempty"`
	Data             map[string]any `json:"data,omitempty"`
}

// SetUnhandled indicates that the exception is an unhandled exception, i.e.
// from a panic.
func (m *Mechanism) SetUnhandled() {
	m.Handled = Pointer(false)
}

// Exception specifies an error that occurred.
type Exception struct {
	Type       string      `json:"type,omitempty"`  // used as the main issue title
	Value      string      `json:"value,omitempty"` // used as the main issue subtitle
	Module     string      `json:"module,omitempty"`
	ThreadID   uint64      `json:"thread_id,omitempty"`
	Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
	Mechanism  *Mechanism  `json:"mechanism,omitempty"`
}

// SDKMetaData is a struct to stash data which is needed at some point in the SDK's event processing pipeline
// but which shouldn't get send to Sentry.
type SDKMetaData struct {
	dsc DynamicSamplingContext
}

// Contains information about how the name of the transaction was determined.
type TransactionInfo struct {
	Source TransactionSource `json:"source,omitempty"`
}

// The DebugMeta interface is not used in Golang apps, but may be populated
// when proxying Events from other platforms, like iOS, Android, and the
// Web.  (See: https://develop.sentry.dev/sdk/event-payloads/debugmeta/ ).
type DebugMeta struct {
	SdkInfo *DebugMetaSdkInfo `json:"sdk_info,omitempty"`
	Images  []DebugMetaImage  `json:"images,omitempty"`
}

type DebugMetaSdkInfo struct {
	SdkName           string `json:"sdk_name,omitempty"`
	VersionMajor      int    `json:"version_major,omitempty"`
	VersionMinor      int    `json:"version_minor,omitempty"`
	VersionPatchlevel int    `json:"version_patchlevel,omitempty"`
}

type DebugMetaImage struct {
	Type        string `json:"type,omitempty"`         // all
	ImageAddr   string `json:"image_addr,omitempty"`   // macho,elf,pe
	ImageSize   int    `json:"image_size,omitempty"`   // macho,elf,pe
	DebugID     string `json:"debug_id,omitempty"`     // macho,elf,pe,wasm,sourcemap
	DebugFile   string `json:"debug_file,omitempty"`   // macho,elf,pe,wasm
	CodeID      string `json:"code_id,omitempty"`      // macho,elf,pe,wasm
	CodeFile    string `json:"code_file,omitempty"`    // macho,elf,pe,wasm,sourcemap
	ImageVmaddr string `json:"image_vmaddr,omitempty"` // macho,elf,pe
	Arch        string `json:"arch,omitempty"`         // macho,elf,pe
	UUID        string `json:"uuid,omitempty"`         // proguard
}

// EventID is a hexadecimal string representing a unique uuid4 for an Event.
// An EventID must be 32 characters long, lowercase and not have any dashes.
type EventID string

type Context = map[string]interface{}

// Event is the fundamental data structure that is sent to Sentry.
type Event struct {
	Breadcrumbs []*Breadcrumb          `json:"breadcrumbs,omitempty"`
	Contexts    map[string]Context     `json:"contexts,omitempty"`
	Dist        string                 `json:"dist,omitempty"`
	Environment string                 `json:"environment,omitempty"`
	EventID     EventID                `json:"event_id,omitempty"`
	Extra       map[string]interface{} `json:"extra,omitempty"`
	Fingerprint []string               `json:"fingerprint,omitempty"`
	Level       Level                  `json:"level,omitempty"`
	Message     string                 `json:"message,omitempty"`
	Platform    string                 `json:"platform,omitempty"`
	Release     string                 `json:"release,omitempty"`
	Sdk         SdkInfo                `json:"sdk,omitempty"`
	ServerName  string                 `json:"server_name,omitempty"`
	Threads     []Thread               `json:"threads,omitempty"`
	Tags        map[string]string      `json:"tags,omitempty"`
	Timestamp   time.Time              `json:"timestamp"`
	Transaction string                 `json:"transaction,omitempty"`
	User        User                   `json:"user,omitempty"`
	Logger      string                 `json:"logger,omitempty"`
	Modules     map[string]string      `json:"modules,omitempty"`
	Request     *Request               `json:"request,omitempty"`
	Exception   []Exception            `json:"exception,omitempty"`
	DebugMeta   *DebugMeta             `json:"debug_meta,omitempty"`
	Attachments []*Attachment          `json:"-"`

	// The fields below are only relevant for transactions.

	Type            string           `json:"type,omitempty"`
	StartTime       time.Time        `json:"start_timestamp"`
	Spans           []*Span          `json:"spans,omitempty"`
	TransactionInfo *TransactionInfo `json:"transaction_info,omitempty"`

	// The fields below are only relevant for crons/check ins

	CheckIn       *CheckIn       `json:"check_in,omitempty"`
	MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"`

	// The fields below are only relevant for logs
	Logs []Log `json:"items,omitempty"`

	// The fields below are not part of the final JSON payload.

	sdkMetaData SDKMetaData
}

// SetException appends the unwrapped errors to the event's exception list.
//
// maxErrorDepth is the maximum depth of the error chain we will look
// into while unwrapping the errors. If maxErrorDepth is -1, we will
// unwrap all errors in the chain.
func (e *Event) SetException(exception error, maxErrorDepth int) {
	if exception == nil {
		return
	}

	exceptions := convertErrorToExceptions(exception, maxErrorDepth)
	if len(exceptions) == 0 {
		return
	}

	e.Exception = exceptions
}

// ToEnvelope converts the Event to a Sentry envelope.
// This includes the event data and any attachments as separate envelope items.
func (e *Event) ToEnvelope(dsn *protocol.Dsn) (*protocol.Envelope, error) {
	return e.ToEnvelopeWithTime(dsn, time.Now())
}

// ToEnvelopeWithTime converts the Event to a Sentry envelope with a specific sentAt time.
// This is primarily useful for testing with predictable timestamps.
func (e *Event) ToEnvelopeWithTime(dsn *protocol.Dsn, sentAt time.Time) (*protocol.Envelope, error) {
	// Create envelope header with trace context
	trace := make(map[string]string)
	if dsc := e.sdkMetaData.dsc; dsc.HasEntries() {
		for k, v := range dsc.Entries {
			trace[k] = v
		}
	}

	header := &protocol.EnvelopeHeader{
		EventID: string(e.EventID),
		SentAt:  sentAt,
		Trace:   trace,
	}

	if dsn != nil {
		header.Dsn = dsn.String()
	}

	header.Sdk = &e.Sdk

	envelope := protocol.NewEnvelope(header)

	eventBody, err := json.Marshal(e)
	if err != nil {
		// Try fallback: remove problematic fields and retry
		e.Breadcrumbs = nil
		e.Contexts = nil
		e.Extra = map[string]interface{}{
			"info": fmt.Sprintf("Could not encode original event as JSON. "+
				"Succeeded by removing Breadcrumbs, Contexts and Extra. "+
				"Please verify the data you attach to the scope. "+
				"Error: %s", err),
		}

		eventBody, err = json.Marshal(e)
		if err != nil {
			return nil, fmt.Errorf("event could not be marshaled even with fallback: %w", err)
		}

		DebugLogger.Printf("Event marshaling succeeded with fallback after removing problematic fields")
	}

	var mainItem *protocol.EnvelopeItem
	switch e.Type {
	case transactionType:
		mainItem = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeTransaction, eventBody)
	case checkInType:
		mainItem = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeCheckIn, eventBody)
	case logEvent.Type:
		mainItem = protocol.NewLogItem(len(e.Logs), eventBody)
	default:
		mainItem = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeEvent, eventBody)
	}

	envelope.AddItem(mainItem)
	for _, attachment := range e.Attachments {
		attachmentItem := protocol.NewAttachmentItem(attachment.Filename, attachment.ContentType, attachment.Payload)
		envelope.AddItem(attachmentItem)
	}

	return envelope, nil
}

// ToEnvelopeItem converts the Event to a Sentry envelope item.
func (e *Event) ToEnvelopeItem() (*protocol.EnvelopeItem, error) {
	eventBody, err := json.Marshal(e)
	if err != nil {
		// Try fallback: remove problematic fields and retry
		e.Breadcrumbs = nil
		e.Contexts = nil
		e.Extra = map[string]interface{}{
			"info": fmt.Sprintf("Could not encode original event as JSON. "+
				"Succeeded by removing Breadcrumbs, Contexts and Extra. "+
				"Please verify the data you attach to the scope. "+
				"Error: %s", err),
		}

		eventBody, err = json.Marshal(e)
		if err != nil {
			return nil, fmt.Errorf("event could not be marshaled even with fallback: %w", err)
		}

		DebugLogger.Printf("Event marshaling succeeded with fallback after removing problematic fields")
	}

	// TODO: all event types should be abstracted to implement EnvelopeItemConvertible and convert themselves.
	var item *protocol.EnvelopeItem
	switch e.Type {
	case transactionType:
		item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeTransaction, eventBody)
	case checkInType:
		item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeCheckIn, eventBody)
	case logEvent.Type:
		item = protocol.NewLogItem(len(e.Logs), eventBody)
	default:
		item = protocol.NewEnvelopeItem(protocol.EnvelopeItemTypeEvent, eventBody)
	}

	return item, nil
}

// GetCategory returns the rate limit category for this event.
func (e *Event) GetCategory() ratelimit.Category {
	return e.toCategory()
}

// GetEventID returns the event ID.
func (e *Event) GetEventID() string {
	return string(e.EventID)
}

// GetSdkInfo returns SDK information for the envelope header.
func (e *Event) GetSdkInfo() *protocol.SdkInfo {
	return &e.Sdk
}

// GetDynamicSamplingContext returns trace context for the envelope header.
func (e *Event) GetDynamicSamplingContext() map[string]string {
	trace := make(map[string]string)
	if dsc := e.sdkMetaData.dsc; dsc.HasEntries() {
		for k, v := range dsc.Entries {
			trace[k] = v
		}
	}
	return trace
}

// TODO: Event.Contexts map[string]interface{} => map[string]EventContext,
// to prevent accidentally storing T when we mean *T.
// For example, the TraceContext must be stored as *TraceContext to pick up the
// MarshalJSON method (and avoid copying).
// type EventContext interface{ EventContext() }

// MarshalJSON converts the Event struct to JSON.
func (e *Event) MarshalJSON() ([]byte, error) {
	// We want to omit time.Time zero values, otherwise the server will try to
	// interpret dates too far in the past. However, encoding/json doesn't
	// support the "omitempty" option for struct types. See
	// https://golang.org/issues/11939.
	//
	// We overcome the limitation and achieve what we want by shadowing fields
	// and a few type tricks.
	if e.Type == transactionType {
		return e.transactionMarshalJSON()
	}

	if e.Type == checkInType {
		return e.checkInMarshalJSON()
	}
	return e.defaultMarshalJSON()
}

func (e *Event) defaultMarshalJSON() ([]byte, error) {
	// event aliases Event to allow calling json.Marshal without an infinite
	// loop. It preserves all fields while none of the attached methods.
	type event Event

	// errorEvent is like Event with shadowed fields for customizing JSON
	// marshaling.
	type errorEvent struct {
		*event

		// Timestamp shadows the original Timestamp field. It allows us to
		// include the timestamp when non-zero and omit it otherwise.
		Timestamp json.RawMessage `json:"timestamp,omitempty"`

		// The fields below are not part of error events and only make sense to
		// be sent for transactions. They shadow the respective fields in Event
		// and are meant to remain nil, triggering the omitempty behavior.

		Type            json.RawMessage `json:"type,omitempty"`
		StartTime       json.RawMessage `json:"start_timestamp,omitempty"`
		Spans           json.RawMessage `json:"spans,omitempty"`
		TransactionInfo json.RawMessage `json:"transaction_info,omitempty"`
	}

	x := errorEvent{event: (*event)(e)}
	if !e.Timestamp.IsZero() {
		b, err := e.Timestamp.MarshalJSON()
		if err != nil {
			return nil, err
		}
		x.Timestamp = b
	}
	return json.Marshal(x)
}

func (e *Event) transactionMarshalJSON() ([]byte, error) {
	// event aliases Event to allow calling json.Marshal without an infinite
	// loop. It preserves all fields while none of the attached methods.
	type event Event

	// transactionEvent is like Event with shadowed fields for customizing JSON
	// marshaling.
	type transactionEvent struct {
		*event

		// The fields below shadow the respective fields in Event. They allow us
		// to include timestamps when non-zero and omit them otherwise.

		StartTime json.RawMessage `json:"start_timestamp,omitempty"`
		Timestamp json.RawMessage `json:"timestamp,omitempty"`
	}

	x := transactionEvent{event: (*event)(e)}
	if !e.Timestamp.IsZero() {
		b, err := e.Timestamp.MarshalJSON()
		if err != nil {
			return nil, err
		}
		x.Timestamp = b
	}
	if !e.StartTime.IsZero() {
		b, err := e.StartTime.MarshalJSON()
		if err != nil {
			return nil, err
		}
		x.StartTime = b
	}
	return json.Marshal(x)
}

func (e *Event) checkInMarshalJSON() ([]byte, error) {
	checkIn := serializedCheckIn{
		CheckInID:     string(e.CheckIn.ID),
		MonitorSlug:   e.CheckIn.MonitorSlug,
		Status:        e.CheckIn.Status,
		Duration:      e.CheckIn.Duration.Seconds(),
		Release:       e.Release,
		Environment:   e.Environment,
		MonitorConfig: nil,
	}

	if e.MonitorConfig != nil {
		checkIn.MonitorConfig = &MonitorConfig{
			Schedule:              e.MonitorConfig.Schedule,
			CheckInMargin:         e.MonitorConfig.CheckInMargin,
			MaxRuntime:            e.MonitorConfig.MaxRuntime,
			Timezone:              e.MonitorConfig.Timezone,
			FailureIssueThreshold: e.MonitorConfig.FailureIssueThreshold,
			RecoveryThreshold:     e.MonitorConfig.RecoveryThreshold,
		}
	}

	return json.Marshal(checkIn)
}

func (e *Event) toCategory() ratelimit.Category {
	switch e.Type {
	case "":
		return ratelimit.CategoryError
	case transactionType:
		return ratelimit.CategoryTransaction
	case logEvent.Type:
		return ratelimit.CategoryLog
	case checkInType:
		return ratelimit.CategoryMonitor
	default:
		return ratelimit.CategoryUnknown
	}
}

// NewEvent creates a new Event.
func NewEvent() *Event {
	return &Event{
		Contexts: make(map[string]Context),
		Extra:    make(map[string]interface{}),
		Tags:     make(map[string]string),
		Modules:  make(map[string]string),
	}
}

// Thread specifies threads that were running at the time of an event.
type Thread struct {
	ID         string      `json:"id,omitempty"`
	Name       string      `json:"name,omitempty"`
	Stacktrace *Stacktrace `json:"stacktrace,omitempty"`
	Crashed    bool        `json:"crashed,omitempty"`
	Current    bool        `json:"current,omitempty"`
}

// EventHint contains information that can be associated with an Event.
type EventHint struct {
	Data               interface{}
	EventID            string
	OriginalException  error
	RecoveredException interface{}
	Context            context.Context
	Request            *http.Request
	Response           *http.Response
}

type Log struct {
	Timestamp  time.Time            `json:"timestamp,omitempty"`
	TraceID    TraceID              `json:"trace_id,omitempty"`
	Level      LogLevel             `json:"level"`
	Severity   int                  `json:"severity_number,omitempty"`
	Body       string               `json:"body,omitempty"`
	Attributes map[string]Attribute `json:"attributes,omitempty"`
}

// ToEnvelopeItem converts the Log to a Sentry envelope item for batching.
func (l *Log) ToEnvelopeItem() (*protocol.EnvelopeItem, error) {
	type logJSON struct {
		Timestamp  *float64                         `json:"timestamp,omitempty"`
		TraceID    string                           `json:"trace_id,omitempty"`
		Level      string                           `json:"level"`
		Severity   int                              `json:"severity_number,omitempty"`
		Body       string                           `json:"body,omitempty"`
		Attributes map[string]protocol.LogAttribute `json:"attributes,omitempty"`
	}

	// Convert time.Time to seconds float if set
	var ts *float64
	if !l.Timestamp.IsZero() {
		sec := float64(l.Timestamp.UnixNano()) / 1e9
		ts = &sec
	}

	attrs := make(map[string]protocol.LogAttribute, len(l.Attributes))
	for k, v := range l.Attributes {
		attrs[k] = protocol.LogAttribute{Value: v.Value, Type: string(v.Type)}
	}

	logData, err := json.Marshal(logJSON{
		Timestamp:  ts,
		TraceID:    l.TraceID.String(),
		Level:      string(l.Level),
		Severity:   l.Severity,
		Body:       l.Body,
		Attributes: attrs,
	})
	if err != nil {
		return nil, err
	}

	return &protocol.EnvelopeItem{
		Header: &protocol.EnvelopeItemHeader{
			Type: protocol.EnvelopeItemTypeLog,
		},
		Payload: logData,
	}, nil
}

// GetCategory returns the rate limit category for logs.
func (l *Log) GetCategory() ratelimit.Category {
	return ratelimit.CategoryLog
}

// GetEventID returns empty string (event ID set when batching).
func (l *Log) GetEventID() string {
	return ""
}

// GetSdkInfo returns nil (SDK info set when batching).
func (l *Log) GetSdkInfo() *protocol.SdkInfo {
	return nil
}

// GetDynamicSamplingContext returns nil (trace context set when batching).
func (l *Log) GetDynamicSamplingContext() map[string]string {
	return nil
}

type AttrType string

const (
	AttributeInvalid AttrType = ""
	AttributeBool    AttrType = "boolean"
	AttributeInt     AttrType = "integer"
	AttributeFloat   AttrType = "double"
	AttributeString  AttrType = "string"
)

type Attribute struct {
	Value any      `json:"value"`
	Type  AttrType `json:"type"`
}
