// Package slogor provides a colorful, allocation-conscious slog.Handler
// close in spirit to log/slog's own TextHandler.
package slogor

import (
	"context"
	"fmt"
	"io"
	"log/slog"
	"runtime"
	"sync"
	"time"
)

// Handler is a slog.Handler that writes Records to an io.Writer as a
// sequence of colorful time, level, source, message and key=value pairs
// separated by spaces and followed by a newline. A Handler is safe for
// concurrent use by multiple goroutines, including concurrent calls to
// Handle, WithAttrs and WithGroup.
type Handler struct {
	opts options
	mu   *sync.Mutex
	w    io.Writer

	// attrsPrefix is the pre-rendered (formatted, colored, quoted) output
	// of every attribute attached via WithAttrs so far. Rendering once at
	// WithAttrs time instead of on every Handle call is the single
	// biggest win for the common "logger with fixed attrs used to log
	// many records" pattern.
	attrsPrefix string
	// groupPrefix is the dot-joined prefix ("g1.g2.") applied to the key
	// of every attribute logged from this point on.
	groupPrefix string
	// groups is the stack of currently open group names, passed to
	// ReplaceAttr as documented by slog.HandlerOptions.
	groups []string
}

// NewHandler creates a Handler that writes to w with the provided Options.
func NewHandler(w io.Writer, fns ...OptionFn) *Handler {
	opts := getDefaultOptions().ConsumeFnOpt(fns...)

	enableVirtualTerminalProcessing(w)

	return &Handler{
		opts: *opts,
		mu:   &sync.Mutex{},
		w:    w,
	}
}

// clone returns a copy of h. The mutex is shared, not copied, so that all
// handlers derived from a common ancestor serialize writes to the same
// underlying writer.
func (h *Handler) clone() *Handler {
	return &Handler{
		opts:        h.opts,
		mu:          h.mu,
		w:           h.w,
		attrsPrefix: h.attrsPrefix,
		groupPrefix: h.groupPrefix,
		groups:      h.groups,
	}
}

// Enabled reports whether the handler handles records at the given level.
// The handler ignores records whose level is lower.
func (h *Handler) Enabled(_ context.Context, level slog.Level) bool {
	return level >= h.opts.level.Level()
}

// WithGroup returns a new handler in which subsequent attributes are
// nested under the given group name via a dot-joined key prefix.
func (h *Handler) WithGroup(name string) slog.Handler {
	if name == "" {
		return h
	}

	h2 := h.clone()
	h2.groupPrefix = h.groupPrefix + name + "."
	// Cap the parent's capacity so this append always allocates a fresh
	// backing array instead of potentially aliasing (and silently
	// corrupting) a sibling handler derived from the same parent.
	h2.groups = append(h.groups[:len(h.groups):len(h.groups)], name)

	return h2
}

// WithAttrs returns a new handler with attrs rendered once and prepended
// to every subsequent record it handles.
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
	if len(attrs) == 0 {
		return h
	}

	bufp := allocBuf()
	buf := *bufp

	defer func() {
		*bufp = buf
		freeBuf(bufp)
	}()

	for _, attr := range attrs {
		buf = appendAttr(buf, &h.opts, h.groupPrefix, h.groups, attr)
	}

	h2 := h.clone()
	h2.attrsPrefix = h.attrsPrefix + string(buf)

	return h2
}

// Handle processes the log record and writes it to the writer with appropriate formatting.
func (h *Handler) Handle(_ context.Context, record slog.Record) error {
	bufp := allocBuf() // Allocate a buffer for writing the log record.
	buf := *bufp

	defer func() {
		*bufp = buf
		freeBuf(bufp)
	}()

	rep := h.opts.replaceAttr

	buf = h.addTime(buf, record.Time, rep)
	buf = h.addLogLevel(buf, record.Level, rep)

	if h.opts.showSource && record.PC != 0 {
		buf = h.addSource(buf, record.PC, rep)
	}

	buf = addMessage(buf, record.Message, rep)

	buf = append(buf, h.attrsPrefix...)

	record.Attrs(func(attr slog.Attr) bool {
		buf = appendAttr(buf, &h.opts, h.groupPrefix, h.groups, attr)

		return true
	})

	// Replace the latest trailing space by an EOL, or write a bare
	// newline if ReplaceAttr dropped everything.
	if len(buf) == 0 {
		buf = append(buf, '\n')
	} else {
		buf[len(buf)-1] = '\n'
	}

	// Lock the handler for writing and unlock once finished.
	h.mu.Lock()
	defer h.mu.Unlock()

	if _, err := h.w.Write(buf); err != nil {
		return fmt.Errorf("slogor: write log record: %w", err)
	}

	return nil
}

func (h *Handler) addTime(buf []byte, t time.Time, rep replaceAttrFunc) []byte {
	if h.opts.timeFormat == "" || t.IsZero() {
		return buf
	}

	if rep == nil {
		buf = h.opts.color(buf, faint)
		buf = t.AppendFormat(buf, h.opts.timeFormat)
		buf = h.opts.color(buf, normalIntensity)

		return append(buf, ' ')
	}

	// strip monotonic reading to match the value slog itself would pass.
	a := rep(nil, slog.Time(slog.TimeKey, t.Round(0)))
	if a.Equal(slog.Attr{}) {
		return buf
	}

	buf = appendValue(buf, a.Value.Resolve())

	return append(buf, ' ')
}

func (h *Handler) addLogLevel(buf []byte, level slog.Level, rep replaceAttrFunc) []byte {
	if rep == nil {
		buf = h.opts.color(buf, levelColor(level))
		buf = append(buf, h.opts.levelString(level)...)
		buf = h.opts.color(buf, reset)

		return append(buf, ' ')
	}

	a := rep(nil, slog.Any(slog.LevelKey, level))
	if a.Equal(slog.Attr{}) {
		return buf
	}

	a.Value = a.Value.Resolve()

	if lvl, ok := a.Value.Any().(slog.Level); ok {
		buf = h.opts.color(buf, levelColor(lvl))
		buf = append(buf, h.opts.levelString(lvl)...)
		buf = h.opts.color(buf, reset)
	} else {
		buf = appendValue(buf, a.Value)
	}

	return append(buf, ' ')
}

func (h *Handler) addSource(buf []byte, pc uintptr, rep replaceAttrFunc) []byte {
	frame, _ := runtime.CallersFrames([]uintptr{pc}).Next()
	if frame.File == "" {
		return buf
	}

	if rep == nil {
		buf = h.opts.color(buf, fgBlue)
		buf = h.opts.color(buf, underline)
		buf = appendSource(buf, frame.File, frame.Line)
		buf = h.opts.color(buf, reset)

		return append(buf, ' ')
	}

	src := &slog.Source{Function: frame.Function, File: frame.File, Line: frame.Line}

	a := rep(nil, slog.Any(slog.SourceKey, src))
	if a.Equal(slog.Attr{}) {
		return buf
	}

	buf = appendValue(buf, a.Value.Resolve())

	return append(buf, ' ')
}

func addMessage(buf []byte, message string, rep replaceAttrFunc) []byte {
	if rep == nil {
		buf = append(buf, message...)

		return append(buf, ' ')
	}

	a := rep(nil, slog.String(slog.MessageKey, message))
	if a.Equal(slog.Attr{}) {
		return buf
	}

	buf = appendValue(buf, a.Value.Resolve())

	return append(buf, ' ')
}

// Err creates a slog.Attr wrapping err under the "err" key.
func Err(err error) slog.Attr {
	return slog.Any("err", err)
}
