// Package logging configures the server's human-readable, structured log output.
package logging

import (
	"context"
	"io"
	"log/slog"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"
)

// ParseLevel returns a supported slog level, defaulting to INFO for empty or invalid
// values. Keeping this forgiving prevents a typo in Docker configuration from stopping
// the gateway.
func ParseLevel(value string) slog.Level {
	switch strings.ToUpper(strings.TrimSpace(value)) {
	case "DEBUG":
		return slog.LevelDebug
	case "WARN", "WARNING":
		return slog.LevelWarn
	case "ERROR":
		return slog.LevelError
	default:
		return slog.LevelInfo
	}
}

// Format selects how a record is rendered. The wire shape of a log line is operational
// configuration, not a code decision: a person tailing `docker compose logs` and a
// collector shipping the same stream elsewhere want different things from it.
type Format int

const (
	// FormatConsole is the default: one aligned, human-readable line per event.
	FormatConsole Format = iota
	// FormatLogfmt is slog's own `time=… level=… msg=…` text output.
	FormatLogfmt
	// FormatJSON is one object per line, for a log collector.
	FormatJSON
)

// ParseFormat is forgiving for the same reason [ParseLevel] is — a typo in Docker
// configuration must not stop the gateway.
func ParseFormat(value string) Format {
	switch strings.ToUpper(strings.TrimSpace(value)) {
	case "JSON":
		return FormatJSON
	case "LOGFMT", "TEXT":
		return FormatLogfmt
	default:
		return FormatConsole
	}
}

// New returns a logger suited to `docker compose logs`: one compact line per event.
func New(w io.Writer, level slog.Leveler) *slog.Logger {
	logger, _ := NewBuffered(w, level, 0, FormatConsole)
	return logger
}

// Event is the browser-safe representation of one structured server log record.
type Event struct {
	Sequence   int64             `json:"sequence"`
	OccurredAt time.Time         `json:"occurredAt"`
	Level      string            `json:"level"`
	Message    string            `json:"message"`
	Attributes map[string]string `json:"attributes,omitempty"`
}

// EventPage is cursor based so an admin browser can drain bursts without repeatedly
// downloading records it has already rendered.
type EventPage struct {
	Events  []Event `json:"events"`
	Next    int64   `json:"next"`
	Oldest  int64   `json:"oldest"`
	Latest  int64   `json:"latest"`
	Dropped int64   `json:"dropped"`
	HasMore bool    `json:"hasMore"`
}

// Buffer is a bounded, concurrency-safe ring of recent structured log records.
// Bounding is important: a broken TV can generate traffic indefinitely. The configured
// ring retains enough context for an operator without growing or shifting on every write.
type Buffer struct {
	mu       sync.RWMutex
	capacity int
	events   []Event
	start    int
	next     atomic.Int64
}

// ParseCapacity returns a non-negative log buffer capacity from configuration.
func ParseCapacity(value string, fallback int) int {
	capacity, err := strconv.Atoi(strings.TrimSpace(value))
	if err != nil || capacity < 0 {
		return fallback
	}
	return capacity
}

// NewBuffered writes lines in the requested format and mirrors accepted records into a
// ring buffer, which is what the admin page's live log reads.
func NewBuffered(
	w io.Writer, level slog.Leveler, capacity int, format Format,
) (*slog.Logger, *Buffer) {
	options := &slog.HandlerOptions{
		Level: level,
		ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr {
			if attr.Key == slog.TimeKey {
				return slog.String(slog.TimeKey, attr.Value.Time().UTC().Format(time.RFC3339))
			}
			return attr
		},
	}
	var written slog.Handler
	switch format {
	case FormatJSON:
		written = slog.NewJSONHandler(w, options)
	case FormatLogfmt:
		written = slog.NewTextHandler(w, options)
	default:
		written = newConsoleHandler(w, level)
	}
	buffer := &Buffer{capacity: capacity}
	if capacity <= 0 {
		return slog.New(written), buffer
	}
	return slog.New(&captureHandler{next: written, buffer: buffer, level: level}), buffer
}

type captureHandler struct {
	next   slog.Handler
	buffer *Buffer
	level  slog.Leveler
	attrs  []slog.Attr
	groups []string
}

func (h *captureHandler) Enabled(ctx context.Context, level slog.Level) bool {
	return level >= h.level.Level() && h.next.Enabled(ctx, level)
}

func (h *captureHandler) Handle(ctx context.Context, record slog.Record) error {
	attributes := make(map[string]string, record.NumAttrs()+len(h.attrs))
	for _, attr := range h.attrs {
		addAttribute(attributes, h.groups, attr)
	}
	record.Attrs(func(attr slog.Attr) bool {
		addAttribute(attributes, h.groups, attr)
		return true
	})
	h.buffer.append(Event{
		OccurredAt: record.Time.UTC(),
		Level:      record.Level.String(),
		Message:    record.Message,
		Attributes: attributes,
	})
	return h.next.Handle(ctx, record)
}

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

func (h *captureHandler) WithGroup(name string) slog.Handler {
	clone := *h
	clone.next = h.next.WithGroup(name)
	clone.groups = append(append([]string{}, h.groups...), name)
	return &clone
}

func addAttribute(target map[string]string, groups []string, attr slog.Attr) {
	attr.Value = attr.Value.Resolve()
	if attr.Equal(slog.Attr{}) {
		return
	}
	key := strings.Join(append(append([]string{}, groups...), attr.Key), ".")
	if attr.Value.Kind() == slog.KindGroup {
		for _, child := range attr.Value.Group() {
			addAttribute(target, append(groups, attr.Key), child)
		}
		return
	}
	target[key] = attributeValue(attr.Value)
}

func (b *Buffer) append(event Event) {
	event.Sequence = b.next.Add(1)
	b.mu.Lock()
	defer b.mu.Unlock()
	if len(b.events) == b.capacity {
		b.events[b.start] = event
		b.start = (b.start + 1) % b.capacity
		return
	}
	b.events = append(b.events, event)
}

// Events returns records strictly newer than after, up to limit. If the caller fell
// behind the ring, Dropped reports the gap and delivery resumes at the oldest record.
func (b *Buffer) Events(after int64, limit int) EventPage {
	if limit < 1 {
		limit = 250
	}
	if limit > 1000 {
		limit = 1000
	}
	b.mu.RLock()
	defer b.mu.RUnlock()

	latest := b.next.Load()
	page := EventPage{Next: after, Latest: latest, Events: []Event{}}
	if len(b.events) == 0 {
		return page
	}
	page.Oldest = b.events[b.start].Sequence
	if after < page.Oldest-1 {
		page.Dropped = page.Oldest - after - 1
		after = page.Oldest - 1
	}
	start := len(b.events)
	for i := range b.events {
		event := b.events[(b.start+i)%len(b.events)]
		if event.Sequence > after {
			start = i
			break
		}
	}
	end := min(start+limit, len(b.events))
	for i := start; i < end; i++ {
		page.Events = append(page.Events, b.events[(b.start+i)%len(b.events)])
	}
	if len(page.Events) > 0 {
		page.Next = page.Events[len(page.Events)-1].Sequence
	}
	page.HasMore = page.Next < latest
	return page
}
