package logger

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

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

type slogZapHandler struct {
	logger *zap.Logger
	attrs  []slog.Attr
	groups []string
}

func newSlogZapHandler(logger *zap.Logger) slog.Handler {
	if logger == nil {
		logger = zap.NewNop()
	}
	return &slogZapHandler{
		logger: logger,
		attrs:  make([]slog.Attr, 0, 8),
		groups: make([]string, 0, 4),
	}
}

func (h *slogZapHandler) Enabled(_ context.Context, level slog.Level) bool {
	switch {
	case level >= slog.LevelError:
		return h.logger.Core().Enabled(LevelError)
	case level >= slog.LevelWarn:
		return h.logger.Core().Enabled(LevelWarn)
	case level <= slog.LevelDebug:
		return h.logger.Core().Enabled(LevelDebug)
	default:
		return h.logger.Core().Enabled(LevelInfo)
	}
}

func (h *slogZapHandler) Handle(_ context.Context, record slog.Record) error {
	fields := make([]zap.Field, 0, len(h.attrs)+record.NumAttrs()+3)
	fields = append(fields, slogAttrsToZapFields(h.groups, h.attrs)...)
	record.Attrs(func(attr slog.Attr) bool {
		fields = append(fields, slogAttrToZapField(h.groups, attr))
		return true
	})

	entry := h.logger.With(fields...)
	switch {
	case record.Level >= slog.LevelError:
		entry.Error(record.Message)
	case record.Level >= slog.LevelWarn:
		entry.Warn(record.Message)
	case record.Level <= slog.LevelDebug:
		entry.Debug(record.Message)
	default:
		entry.Info(record.Message)
	}
	return nil
}

func (h *slogZapHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	next := *h
	next.attrs = append(append([]slog.Attr{}, h.attrs...), attrs...)
	return &next
}

func (h *slogZapHandler) WithGroup(name string) slog.Handler {
	name = strings.TrimSpace(name)
	if name == "" {
		return h
	}
	next := *h
	next.groups = append(append([]string{}, h.groups...), name)
	return &next
}

func slogAttrsToZapFields(groups []string, attrs []slog.Attr) []zap.Field {
	fields := make([]zap.Field, 0, len(attrs))
	for _, attr := range attrs {
		fields = append(fields, slogAttrToZapField(groups, attr))
	}
	return fields
}

func slogAttrToZapField(groups []string, attr slog.Attr) zap.Field {
	if len(groups) > 0 {
		attr.Key = strings.Join(append(append([]string{}, groups...), attr.Key), ".")
	}
	value := attr.Value.Resolve()
	switch value.Kind() {
	case slog.KindBool:
		return zap.Bool(attr.Key, value.Bool())
	case slog.KindInt64:
		return zap.Int64(attr.Key, value.Int64())
	case slog.KindUint64:
		return zap.Uint64(attr.Key, value.Uint64())
	case slog.KindFloat64:
		return zap.Float64(attr.Key, value.Float64())
	case slog.KindDuration:
		return zap.Duration(attr.Key, value.Duration())
	case slog.KindTime:
		return zap.Time(attr.Key, value.Time())
	case slog.KindString:
		return zap.String(attr.Key, value.String())
	case slog.KindGroup:
		groupFields := make([]zap.Field, 0, len(value.Group()))
		for _, nested := range value.Group() {
			groupFields = append(groupFields, slogAttrToZapField(nil, nested))
		}
		return zap.Object(attr.Key, zapObjectFields(groupFields))
	case slog.KindAny:
		if t, ok := value.Any().(time.Time); ok {
			return zap.Time(attr.Key, t)
		}
		return zap.Any(attr.Key, value.Any())
	default:
		return zap.String(attr.Key, value.String())
	}
}

type zapObjectFields []zap.Field

func (z zapObjectFields) MarshalLogObject(enc zapcore.ObjectEncoder) error {
	for _, field := range z {
		field.AddTo(enc)
	}
	return nil
}
