package log

import (
	"context"

	"github.com/sirupsen/logrus"
	"go.opencensus.io/trace"
)

type entryContextKeyType int

const _entryContextKey entryContextKeyType = iota

var (
	// L is the default, blank logging entry. WithField and co. all return a copy
	// of the original entry, so this will not leak fields between calls.
	//
	// Do NOT modify fields directly, as that will corrupt state for all users and
	// is not thread safe.
	// Instead, use `L.With*` or `L.Dup()`. Or `G(context.Background())`.
	L = logrus.NewEntry(logrus.StandardLogger())

	// G is an alias for GetEntry
	G = GetEntry

	// S is an alias for SetEntry
	S = SetEntry

	// U is an alias for UpdateContext
	U = UpdateContext
)

// GetEntry returns a `logrus.Entry` stored in the context, if one exists.
// Otherwise, it returns a default entry that points to the current context.
//
// Note: if the a new entry is returned, it will reference the passed in context.
// However, existing contexts may be stored in parent contexts and additionally reference
// earlier contexts.
// Use `UpdateContext` to update the entry and context.
func GetEntry(ctx context.Context) *logrus.Entry {
	entry := fromContext(ctx)

	if entry == nil {
		entry = L.WithContext(ctx)
	}

	return entry
}

// SetEntry updates the log entry in the context with the provided fields, and
// returns both. It is equivalent to:
//
//	entry := GetEntry(ctx).WithFields(fields)
//	ctx = WithContext(ctx, entry)
//
// See WithContext for more information.
func SetEntry(ctx context.Context, fields logrus.Fields) (context.Context, *logrus.Entry) {
	e := GetEntry(ctx)
	if len(fields) > 0 {
		e = e.WithFields(fields)
	}
	return WithContext(ctx, e)
}

// UpdateContext extracts the log entry from the context, and, if the entry's
// context points to a parent's of the current context, ands the entry
// to the most recent context. It is equivalent to:
//
//	entry := GetEntry(ctx)
//	ctx = WithContext(ctx, entry)
//
// This allows the entry to reference the most recent context and any new
// values (such as span contexts) added to it.
//
// See WithContext for more information.
func UpdateContext(ctx context.Context) context.Context {
	// there is no way to check its ctx (and not one of its parents) that contains `e`
	// so, at a slight cost, force add `e` to the context
	ctx, _ = WithContext(ctx, GetEntry(ctx))
	return ctx
}

// WithContext returns a context that contains the provided log entry.
// The entry can be extracted with `GetEntry` (`G`)
//
// The entry in the context is a copy of `entry` (generated by `entry.WithContext`)
func WithContext(ctx context.Context, entry *logrus.Entry) (context.Context, *logrus.Entry) {
	// regardless of the order, entry.Context != GetEntry(ctx)
	// here, the returned entry will reference the supplied context
	entry = entry.WithContext(ctx)
	ctx = context.WithValue(ctx, _entryContextKey, entry)

	return ctx, entry
}

// Copy extracts the tracing Span and logging entry from the src Context, if they
// exist, and adds them to the dst Context.
//
// This is useful to share tracing and logging between contexts, but not the
// cancellation. For example, if the src Context has been cancelled but cleanup
// operations triggered by the cancellation require a non-cancelled context to
// execute.
func Copy(dst context.Context, src context.Context) context.Context {
	if s := trace.FromContext(src); s != nil {
		dst = trace.NewContext(dst, s)
	}

	if e := fromContext(src); e != nil {
		dst, _ = WithContext(dst, e)
	}

	return dst
}

func fromContext(ctx context.Context) *logrus.Entry {
	e, _ := ctx.Value(_entryContextKey).(*logrus.Entry)
	return e
}
