package log

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"os"

	"github.com/charmbracelet/log"
	"go.opentelemetry.io/contrib/bridges/otelslog"
	otellog "go.opentelemetry.io/otel/log"
	"go.opentelemetry.io/otel/trace"
)

type handlerOperation struct {
	attrs []slog.Attr
	group string
}

type namedHandler struct {
	slog.Handler
	name       string
	format     string
	writer     io.Writer
	hasTraceID bool
	hasSpanID  bool
	operations []handlerOperation
}

func (h *namedHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	hasTraceID := h.hasTraceID
	hasSpanID := h.hasSpanID
	for _, attr := range attrs {
		switch attr.Key {
		case "trace_id":
			hasTraceID = true
		case "span_id":
			hasSpanID = true
		}
	}
	operations := append(h.operations[:len(h.operations):len(h.operations)], handlerOperation{
		attrs: append([]slog.Attr(nil), attrs...),
	})
	return &namedHandler{
		Handler:    h.Handler.WithAttrs(attrs),
		name:       h.name,
		format:     h.format,
		writer:     h.writer,
		hasTraceID: hasTraceID,
		hasSpanID:  hasSpanID,
		operations: operations,
	}
}

func (h *namedHandler) WithGroup(name string) slog.Handler {
	operations := append(h.operations[:len(h.operations):len(h.operations)], handlerOperation{group: name})
	return &namedHandler{
		Handler:    h.Handler.WithGroup(name),
		name:       h.name,
		format:     h.format,
		writer:     h.writer,
		hasTraceID: h.hasTraceID,
		hasSpanID:  h.hasSpanID,
		operations: operations,
	}
}

func (h *namedHandler) WithName(name string) slog.Handler {
	next, err := newHandler(h.writer, name, h.format)
	if err != nil {
		return h
	}
	for _, operation := range h.operations {
		if operation.group != "" {
			next = next.WithGroup(operation.group)
		} else {
			next = next.WithAttrs(operation.attrs)
		}
	}
	return next
}

func (h *namedHandler) Handle(ctx context.Context, r slog.Record) error {
	spanContext := trace.SpanContextFromContext(ctx)
	if spanContext.IsValid() && spanContext.IsSampled() {
		hasTraceID := h.hasTraceID
		hasSpanID := h.hasSpanID
		if !hasTraceID || !hasSpanID {
			r.Attrs(func(a slog.Attr) bool {
				if a.Key == "trace_id" {
					hasTraceID = true
				}
				if a.Key == "span_id" {
					hasSpanID = true
				}
				return !hasTraceID || !hasSpanID
			})
		}
		var attrs []slog.Attr
		if !hasTraceID {
			attrs = append(attrs, slog.String("trace_id", spanContext.TraceID().String()))
		}
		if !hasSpanID {
			attrs = append(attrs, slog.String("span_id", spanContext.SpanID().String()))
		}
		if len(attrs) > 0 {
			r.AddAttrs(attrs...)
		}
	}
	return h.Handler.Handle(ctx, r)
}

type fanoutHandler struct {
	primary slog.Handler
	others  []slog.Handler
}

func (h *fanoutHandler) Enabled(ctx context.Context, level slog.Level) bool {
	if h.primary.Enabled(ctx, level) {
		return true
	}
	for _, other := range h.others {
		if other.Enabled(ctx, level) {
			return true
		}
	}
	return false
}

func (h *fanoutHandler) Handle(ctx context.Context, r slog.Record) error {
	var errs []error
	if h.primary.Enabled(ctx, r.Level) {
		if err := h.primary.Handle(ctx, r); err != nil {
			errs = append(errs, err)
		}
	}
	for _, other := range h.others {
		if other.Enabled(ctx, r.Level) {
			if err := other.Handle(ctx, r); err != nil {
				errs = append(errs, err)
			}
		}
	}
	if len(errs) > 0 {
		return errors.Join(errs...)
	}
	return nil
}

func (h *fanoutHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	others := make([]slog.Handler, len(h.others))
	for i, other := range h.others {
		others[i] = other.WithAttrs(attrs)
	}
	return &fanoutHandler{
		primary: h.primary.WithAttrs(attrs),
		others:  others,
	}
}

func (h *fanoutHandler) WithGroup(name string) slog.Handler {
	others := make([]slog.Handler, len(h.others))
	for i, other := range h.others {
		others[i] = other.WithGroup(name)
	}
	return &fanoutHandler{
		primary: h.primary.WithGroup(name),
		others:  others,
	}
}

func (h *fanoutHandler) WithName(name string) slog.Handler {
	newPrimary := h.primary
	if nh, ok := h.primary.(interface{ WithName(string) slog.Handler }); ok {
		newPrimary = nh.WithName(name)
	}
	others := make([]slog.Handler, len(h.others))
	for i, other := range h.others {
		if nh, ok := other.(interface{ WithName(string) slog.Handler }); ok {
			others[i] = nh.WithName(name)
		} else {
			others[i] = other
		}
	}
	return &fanoutHandler{
		primary: newPrimary,
		others:  others,
	}
}

func NewFanoutHandler(primary slog.Handler, others ...slog.Handler) slog.Handler {
	var validOthers []slog.Handler
	for _, other := range others {
		if other != nil {
			validOthers = append(validOthers, other)
		}
	}
	if len(validOthers) == 0 {
		return primary
	}
	return &fanoutHandler{
		primary: primary,
		others:  validOthers,
	}
}

type otelHandler struct {
	slog.Handler
	provider   otellog.LoggerProvider
	name       string
	operations []handlerOperation
}

func (h *otelHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	operations := append(h.operations[:len(h.operations):len(h.operations)], handlerOperation{
		attrs: append([]slog.Attr(nil), attrs...),
	})
	return &otelHandler{
		Handler:    h.Handler.WithAttrs(attrs),
		provider:   h.provider,
		name:       h.name,
		operations: operations,
	}
}

func (h *otelHandler) WithGroup(name string) slog.Handler {
	operations := append(h.operations[:len(h.operations):len(h.operations)], handlerOperation{group: name})
	return &otelHandler{
		Handler:    h.Handler.WithGroup(name),
		provider:   h.provider,
		name:       h.name,
		operations: operations,
	}
}

func (h *otelHandler) WithName(newName string) slog.Handler {
	var opts []otelslog.Option
	if h.provider != nil {
		opts = append(opts, otelslog.WithLoggerProvider(h.provider))
	}
	var handler slog.Handler = otelslog.NewHandler(newName, opts...)
	for _, operation := range h.operations {
		if operation.group != "" {
			handler = handler.WithGroup(operation.group)
		} else {
			handler = handler.WithAttrs(operation.attrs)
		}
	}
	return &otelHandler{
		Handler:    handler,
		provider:   h.provider,
		name:       newName,
		operations: h.operations,
	}
}

func NewOtelHandler(provider otellog.LoggerProvider, name string) slog.Handler {
	var opts []otelslog.Option
	if provider != nil {
		opts = append(opts, otelslog.WithLoggerProvider(provider))
	}
	h := otelslog.NewHandler(name, opts...)
	return &otelHandler{
		Handler:  h,
		provider: provider,
		name:     name,
	}
}

func newHandler(w io.Writer, name, format string) (slog.Handler, error) {
	var handler slog.Handler
	switch format {
	case "", "text":
		handler = log.NewWithOptions(w, log.Options{
			ReportTimestamp: true,
			Prefix:          name,
			Level:           log.DebugLevel,
		})
		format = "text"
	case "json":
		handler = slog.NewJSONHandler(w, &slog.HandlerOptions{
			Level: slog.LevelDebug,
		}).WithAttrs([]slog.Attr{slog.String("logger", name)})
	default:
		return nil, fmt.Errorf("unknown log format %q", format)
	}
	return &namedHandler{
		Handler: handler,
		name:    name,
		format:  format,
		writer:  w,
	}, nil
}

func NewHandler(name string) slog.Handler {
	handler, _ := newHandler(os.Stderr, name, "text")
	return handler
}

func New(name string) *slog.Logger {
	return slog.New(NewHandler(name))
}

func NewWithFormat(name, format string) (*slog.Logger, error) {
	return newWithFormatWriter(os.Stderr, name, format)
}

func newWithFormatWriter(w io.Writer, name, format string) (*slog.Logger, error) {
	handler, err := newHandler(w, name, format)
	if err != nil {
		return nil, err
	}
	return slog.New(handler), nil
}

func NewContext(ctx context.Context, name string) context.Context {
	return IntoContext(ctx, New(name))
}

type ctxKey struct{}

// IntoContext adds a logger to a context. Use FromContext to
// pull the logger out.
func IntoContext(ctx context.Context, l *slog.Logger) context.Context {
	return context.WithValue(ctx, ctxKey{}, l)
}

// FromContext returns a logger from a context.Context;
// if the passed context is nil, we return the default slog
// logger.
func FromContext(ctx context.Context) *slog.Logger {
	if ctx != nil {
		v := ctx.Value(ctxKey{})
		if v == nil {
			return slog.Default()
		}
		return v.(*slog.Logger)
	}

	return slog.Default()
}

// sublogger derives a new logger from an existing one by appending a suffix to its prefix.
func SubLogger(base *slog.Logger, suffix string) *slog.Logger {
	if base == nil {
		return New(suffix)
	}
	if handler, ok := base.Handler().(*namedHandler); ok {
		name := suffix
		if handler.name != "" {
			name = handler.name + "/" + suffix
		}
		return slog.New(handler.WithName(name))
	}
	if fh, ok := base.Handler().(*fanoutHandler); ok {
		var newName string
		if ph, ok := fh.primary.(*namedHandler); ok {
			if ph.name != "" {
				newName = ph.name + "/" + suffix
			} else {
				newName = suffix
			}
		} else {
			newName = suffix
		}
		return slog.New(fh.WithName(newName))
	}
	if handler, ok := base.Handler().(*log.Logger); ok {
		name := suffix
		if handler.GetPrefix() != "" {
			name = handler.GetPrefix() + "/" + suffix
		}
		return New(name)
	}
	return base.With("component", suffix)
}
