package zerolog

import (
	"context"
	"log/slog"
	"time"
)

// SlogHandler implements the slog.Handler interface using a zerolog.Logger
// as the underlying log backend. This allows code that uses the standard
// library's slog package to route log output through zerolog.
type SlogHandler struct {
	logger Logger
	prefix string // group prefix for nested groups
	attrs  []slog.Attr
}

// NewSlogHandler creates a new slog.Handler that writes log records to the
// given zerolog.Logger. The handler maps slog levels to zerolog levels and
// converts slog attributes to zerolog fields.
func NewSlogHandler(logger Logger) *SlogHandler {
	return &SlogHandler{logger: logger}
}

// Enabled reports whether the handler handles records at the given level.
// It mirrors Logger.should's level and writer checks (without sampling).
func (h *SlogHandler) Enabled(_ context.Context, level slog.Level) bool {
	if h.logger.w == nil {
		return false
	}
	zl := slogToZerologLevel(level)
	if zl < GlobalLevel() {
		return false
	}
	return zl >= h.logger.level
}

// Handle handles the Record. It converts the slog.Record into a zerolog event
// and writes it using the underlying zerolog.Logger.
func (h *SlogHandler) Handle(ctx context.Context, record slog.Record) error {
	zlevel := slogToZerologLevel(record.Level)
	event := h.logger.WithLevel(zlevel)
	if event == nil {
		return nil
	}

	// Propagate slog context to the zerolog event so that hooks
	// relying on Event.GetCtx() (e.g. tracing) can access it.
	if ctx != nil {
		event = event.Ctx(ctx)
	}

	// Add pre-attached attrs from WithAttrs
	for _, a := range h.attrs {
		event = appendSlogAttr(event, a, h.prefix)
	}

	// Add attrs from the record itself
	record.Attrs(func(a slog.Attr) bool {
		event = appendSlogAttr(event, a, h.prefix)
		return true
	})

	// Add timestamp from the slog record, but only if the logger doesn't
	// already have a timestampHook (added via .With().Timestamp()) to
	// avoid duplicate timestamp keys in the output.
	if !record.Time.IsZero() && !h.hasTimestampHook() {
		event.Time(TimestampFieldName, record.Time)
	}

	event.Msg(record.Message)
	return nil
}

// hasTimestampHook reports whether the logger has a timestampHook installed,
// which would cause duplicate timestamp fields if we also emit record.Time.
func (h *SlogHandler) hasTimestampHook() bool {
	for _, hook := range h.logger.hooks {
		if _, ok := hook.(timestampHook); ok {
			return true
		}
	}
	return false
}

// WithAttrs returns a new Handler with the given attributes pre-attached.
// These attributes will be included in every subsequent log record.
func (h *SlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	if len(attrs) == 0 {
		return h
	}
	h2 := h.clone()
	h2.attrs = append(h2.attrs, attrs...)
	return h2
}

// WithGroup returns a new Handler with the given group name. All subsequent
// attributes will be nested under this group name in the output.
func (h *SlogHandler) WithGroup(name string) slog.Handler {
	if name == "" {
		return h
	}
	h2 := h.clone()
	if h2.prefix != "" {
		h2.prefix = h2.prefix + "." + name
	} else {
		h2.prefix = name
	}
	return h2
}

func (h *SlogHandler) clone() *SlogHandler {
	h2 := &SlogHandler{
		logger: h.logger,
		prefix: h.prefix,
	}
	if len(h.attrs) > 0 {
		h2.attrs = make([]slog.Attr, len(h.attrs))
		copy(h2.attrs, h.attrs)
	}
	return h2
}

// slogToZerologLevel maps slog levels to zerolog levels.
//
// slog levels:  Debug=-4, Info=0, Warn=4, Error=8
// zerolog levels: Trace=-1, Debug=0, Info=1, Warn=2, Error=3, Fatal=4, Panic=5
func slogToZerologLevel(level slog.Level) Level {
	switch {
	case level < slog.LevelDebug:
		return TraceLevel
	case level < slog.LevelInfo:
		return DebugLevel
	case level < slog.LevelWarn:
		return InfoLevel
	case level < slog.LevelError:
		return WarnLevel
	default:
		return ErrorLevel
	}
}

// zerologToSlogLevel maps zerolog levels to slog levels.
func zerologToSlogLevel(level Level) slog.Level {
	switch level {
	case TraceLevel:
		return slog.LevelDebug - 4
	case DebugLevel:
		return slog.LevelDebug
	case InfoLevel:
		return slog.LevelInfo
	case WarnLevel:
		return slog.LevelWarn
	case ErrorLevel:
		return slog.LevelError
	case FatalLevel:
		return slog.LevelError + 4
	case PanicLevel:
		return slog.LevelError + 8
	default:
		return slog.LevelInfo
	}
}

// joinPrefix concatenates a prefix and key with a dot separator.
// It avoids allocations when either prefix or key is empty.
func joinPrefix(prefix, key string) string {
	if prefix == "" {
		return key
	}
	if key == "" {
		return prefix
	}
	return prefix + "." + key
}

// appendSlogAttr appends a single slog.Attr to the zerolog event, handling
// type-specific encoding to avoid reflection where possible.
func appendSlogAttr(event *Event, attr slog.Attr, prefix string) *Event {
	if event == nil {
		return event
	}

	// Resolve the attribute to handle LogValuer types.
	// This handles slog.KindLogValuer implicitly by unwrapping
	// any values that implement slog.LogValuer to their resolved form.
	attr.Value = attr.Value.Resolve()

	// For group kinds, handle grouping before key concatenation
	if attr.Value.Kind() == slog.KindGroup {
		attrs := attr.Value.Group()
		if len(attrs) == 0 {
			return event
		}
		groupPrefix := joinPrefix(prefix, attr.Key)
		for _, ga := range attrs {
			event = appendSlogAttr(event, ga, groupPrefix)
		}
		return event
	}

	// Skip empty keys for non-group attributes
	if attr.Key == "" {
		return event
	}

	key := joinPrefix(prefix, attr.Key)
	val := attr.Value

	switch val.Kind() {
	case slog.KindString:
		event = event.Str(key, val.String())
	case slog.KindInt64:
		event = event.Int64(key, val.Int64())
	case slog.KindUint64:
		event = event.Uint64(key, val.Uint64())
	case slog.KindFloat64:
		event = event.Float64(key, val.Float64())
	case slog.KindBool:
		event = event.Bool(key, val.Bool())
	case slog.KindDuration:
		event = event.Dur(key, val.Duration())
	case slog.KindTime:
		event = event.Time(key, val.Time())
	case slog.KindAny:
		v := val.Any()
		switch cv := v.(type) {
		case error:
			event = event.AnErr(key, cv)
		case time.Duration:
			event = event.Dur(key, cv)
		case time.Time:
			event = event.Time(key, cv)
		case []byte:
			event = event.Bytes(key, cv)
		default:
			event = event.Interface(key, v)
		}
	default:
		event = event.Interface(key, val.Any())
	}

	return event
}

// Verify at compile time that SlogHandler satisfies the slog.Handler interface.
var _ slog.Handler = (*SlogHandler)(nil)
