// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package logtest // import "go.opentelemetry.io/otel/log/logtest"

import (
	"context"
	"sync"
	"time"

	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/log"
	"go.opentelemetry.io/otel/log/embedded"
)

type enabledFn func(context.Context, log.EnabledParameters) bool

var defaultEnabledFunc = func(context.Context, log.EnabledParameters) bool {
	return true
}

type config struct {
	enabledFn enabledFn
}

func newConfig(options []Option) config {
	var c config
	for _, opt := range options {
		c = opt.apply(c)
	}

	return c
}

// Option configures a [Recorder].
type Option interface {
	apply(config) config
}

type optFunc func(config) config

func (f optFunc) apply(c config) config { return f(c) }

// WithEnabledFunc allows configuring whether the [Recorder] is enabled for specific log entries or not.
//
// By default, the Recorder is enabled for every log entry.
func WithEnabledFunc(fn func(context.Context, log.EnabledParameters) bool) Option {
	return optFunc(func(c config) config {
		c.enabledFn = fn
		return c
	})
}

// NewRecorder returns a new [Recorder].
func NewRecorder(options ...Option) *Recorder {
	cfg := newConfig(options)

	return &Recorder{
		enabledFn: cfg.enabledFn,
	}
}

// Recording represents the recorded log records snapshot.
type Recording map[Scope][]Record

// Scope represents the instrumentation scope.
type Scope struct {
	// Name is the name of the instrumentation scope. This should be the
	// Go package name of that scope.
	Name string
	// Version is the version of the instrumentation scope.
	Version string
	// SchemaURL of the telemetry emitted by the scope.
	SchemaURL string
	// Attributes of the telemetry emitted by the scope.
	Attributes attribute.Set
}

// Record represents the record alongside its context.
type Record struct {
	// Ensure forward compatibility by explicitly making this not comparable.
	_ [0]func()

	Context           context.Context
	EventName         string
	Timestamp         time.Time
	ObservedTimestamp time.Time
	Severity          log.Severity
	SeverityText      string
	Body              log.Value
	Error             error
	Attributes        []log.KeyValue
}

// Recorder stores all received log records in-memory.
// Recorder implements [log.LoggerProvider].
type Recorder struct {
	// Ensure forward compatibility by explicitly making this not comparable.
	_ [0]func()

	embedded.LoggerProvider

	mu      sync.Mutex
	loggers map[Scope]*logger

	// enabledFn decides whether the recorder should enable logging of a record or not
	enabledFn enabledFn
}

// Compile-time check Recorder implements log.LoggerProvider.
var _ log.LoggerProvider = (*Recorder)(nil)

// Clone returns a deep copy.
func (a Record) Clone() Record {
	b := a
	attrs := make([]log.KeyValue, len(a.Attributes))
	copy(attrs, a.Attributes)
	b.Attributes = attrs
	return b
}

// Logger returns a copy of Recorder as a [log.Logger] with the provided scope
// information.
func (r *Recorder) Logger(name string, opts ...log.LoggerOption) log.Logger {
	cfg := log.NewLoggerConfig(opts...)
	scope := Scope{
		Name:       name,
		Version:    cfg.InstrumentationVersion(),
		SchemaURL:  cfg.SchemaURL(),
		Attributes: cfg.InstrumentationAttributes(),
	}

	r.mu.Lock()
	defer r.mu.Unlock()

	if r.loggers == nil {
		r.loggers = make(map[Scope]*logger)
	}

	l, ok := r.loggers[scope]
	if ok {
		return l
	}
	l = &logger{
		enabledFn: r.enabledFn,
	}
	r.loggers[scope] = l
	return l
}

// Reset clears the in-memory log records for all loggers.
func (r *Recorder) Reset() {
	r.mu.Lock()
	defer r.mu.Unlock()

	for _, l := range r.loggers {
		l.Reset()
	}
}

// Result returns a deep copy of the current in-memory recorded log records.
func (r *Recorder) Result() Recording {
	r.mu.Lock()
	defer r.mu.Unlock()

	res := make(Recording, len(r.loggers))
	for s, l := range r.loggers {
		func() {
			l.mu.Lock()
			defer l.mu.Unlock()
			if l.records == nil {
				res[s] = nil
				return
			}
			recs := make([]Record, len(l.records))
			for i, r := range l.records {
				recs[i] = r.Clone()
			}
			res[s] = recs
		}()
	}
	return res
}

type logger struct {
	embedded.Logger

	mu      sync.Mutex
	records []*Record

	// enabledFn decides whether the recorder should enable logging of a record or not.
	enabledFn enabledFn
}

// Enabled indicates whether a specific record should be stored.
func (l *logger) Enabled(ctx context.Context, param log.EnabledParameters) bool {
	if l.enabledFn == nil {
		return defaultEnabledFunc(ctx, param)
	}

	return l.enabledFn(ctx, param)
}

// Emit stores the log record.
func (l *logger) Emit(ctx context.Context, record log.Record) {
	l.mu.Lock()
	defer l.mu.Unlock()

	attrs := make([]log.KeyValue, 0, record.AttributesLen())
	record.WalkAttributes(func(kv log.KeyValue) bool {
		attrs = append(attrs, kv)
		return true
	})

	r := &Record{
		Context:           ctx,
		EventName:         record.EventName(),
		Timestamp:         record.Timestamp(),
		ObservedTimestamp: record.ObservedTimestamp(),
		Severity:          record.Severity(),
		SeverityText:      record.SeverityText(),
		Body:              record.Body(),
		Error:             record.Err(),
		Attributes:        attrs,
	}

	l.records = append(l.records, r)
}

// Reset clears the in-memory log records.
func (l *logger) Reset() {
	l.mu.Lock()
	defer l.mu.Unlock()

	l.records = nil
}
